mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09431b60c5 | ||
|
|
cbc255813d | ||
|
|
c8be6163e5 | ||
|
|
911e70dfdb | ||
|
|
55ca20bc85 |
No files matched your search
@@ -248,7 +248,7 @@ assert.match(nightly, /startsWith\(matrix\.target, 'aarch64-'\).*runner\.arch ==
|
||||
const nightlyArmMuslSetup = namedStep(nightly, "Install Linux ARM64 musl toolchain");
|
||||
assert.match(nightlyArmMuslSetup, /matrix\.target == 'aarch64-unknown-linux-musl'/);
|
||||
assert.match(nightlyArmMuslSetup, /apt-get install -y binutils musl-tools/);
|
||||
assert.match(nightlyArmMuslSetup, /rustup target add --toolchain stable aarch64-unknown-linux-musl/);
|
||||
assert.match(nightlyArmMuslSetup, /rustup target add aarch64-unknown-linux-musl/);
|
||||
const nightlyArmStaticSmoke = namedStep(
|
||||
nightly,
|
||||
"Verify static Linux ARM64 binary and launch",
|
||||
@@ -373,7 +373,7 @@ assert.deepEqual([...new Set(valuesForKey(artifacts, "target"))].sort(), expecte
|
||||
const releaseMuslBuild = namedStep(artifacts, "Build static Linux binaries (musl)");
|
||||
assert.match(releaseMuslBuild, /endsWith\(matrix\.target, '-unknown-linux-musl'\)/);
|
||||
assert.match(releaseMuslBuild, /apt-get install -y binutils musl-tools/);
|
||||
assert.match(releaseMuslBuild, /rustup target add --toolchain stable \$\{\{ matrix\.target \}\}/);
|
||||
assert.match(releaseMuslBuild, /rustup target add \$\{\{ matrix\.target \}\}/);
|
||||
assert.match(
|
||||
releaseMuslBuild,
|
||||
/cargo build --profile dist --locked --target \$\{\{ matrix\.target \}\} -p codewhale-cli/,
|
||||
@@ -904,6 +904,33 @@ assert.ok(
|
||||
"release parity must allow the full CI suite's cold-build budget",
|
||||
);
|
||||
|
||||
const pinnedToolchain = read("rust-toolchain.toml").match(/^channel\s*=\s*"([^"]+)"/m)[1];
|
||||
assert.match(pinnedToolchain, /^\d+\.\d+\.\d+$/, "rust-toolchain.toml must pin an exact Rust release");
|
||||
const msrv = read("Cargo.toml").match(/^rust-version\s*=\s*"([^"]+)"/m)[1];
|
||||
const workflowDir = path.join(repoRoot, ".github", "workflows");
|
||||
for (const file of fs.readdirSync(workflowDir).filter((name) => /\.ya?ml$/.test(name)).sort()) {
|
||||
const source = read(`.github/workflows/${file}`)
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/(^|\s)#.*$/, ""))
|
||||
.join("\n");
|
||||
const selectors = [
|
||||
...[...source.matchAll(/dtolnay\/rust-toolchain@([^\s]+)/g)].map((m) => ["action ref", m[1]]),
|
||||
...[...source.matchAll(/^\s+toolchain:\s*["']?([^"'\s]+)["']?\s*$/gm)].map((m) => ["toolchain input", m[1]]),
|
||||
...[...source.matchAll(/^\s+rust-version:\s*["']?([^"'\s]+)["']?\s*$/gm)].map((m) => ["rust-version input", m[1]]),
|
||||
];
|
||||
for (const [kind, value] of selectors) {
|
||||
if (/^[0-9a-f]{40}$/.test(value) || value === "master") continue;
|
||||
const expected = file === "ci.yml" && value === msrv ? msrv : pinnedToolchain;
|
||||
assert.equal(value, expected, `${file}: ${kind} ${value} must equal the pinned toolchain ${pinnedToolchain}`);
|
||||
}
|
||||
assert.doesNotMatch(source, /rustup (?:target|component) add --toolchain/, `${file}: rustup adds must follow rust-toolchain.toml`);
|
||||
assert.doesNotMatch(source, new RegExp(`\\bcargo \\+(?!${msrv.replace(".", "\\.")}\\b)\\S+`), `${file}: only the MSRV may select a toolchain with cargo +`);
|
||||
}
|
||||
const msrvJob = ci.slice(ci.indexOf("\n msrv:"), ci.indexOf("\n workflow-rlm-cache:"));
|
||||
assert.match(msrvJob, new RegExp(`RUSTUP_TOOLCHAIN: "${msrv.replace(".", "\\.")}"`), "the MSRV job must override rust-toolchain.toml for every step");
|
||||
assert.match(msrvJob, new RegExp(`toolchain: "${msrv.replace(".", "\\.")}"`));
|
||||
assert.match(msrvJob, new RegExp(`cargo \\+${msrv.replace(".", "\\.")} check --workspace --locked`));
|
||||
|
||||
console.log(
|
||||
"Workflow contracts OK: 6-target/12-asset single-runtime nightly and exact-head 8-target/38-asset release candidate.",
|
||||
);
|
||||
@@ -23,17 +23,17 @@ jobs:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# rust-toolchain.toml pins `channel = "stable"` with no target, so
|
||||
# rust-toolchain.toml pins `channel = "1.100.0"` with no target, so
|
||||
# inside the action's alpine (musl) container rustup resolves it to
|
||||
# `stable-x86_64-unknown-linux-musl` — which the container does not
|
||||
# `1.100.0-x86_64-unknown-linux-musl`, which the container does not
|
||||
# have. Without provisioning, the action's `rustup show` step prints
|
||||
# "error: override toolchain 'stable-x86_64-unknown-linux-musl' is not
|
||||
# "error: override toolchain '1.100.0-x86_64-unknown-linux-musl' is not
|
||||
# installed" and then auto-installs mid-run (network-dependent ~12s
|
||||
# detour, hard failure if the download stalls). `rust-version` makes the
|
||||
# entrypoint run `rustup default stable` (same musl host triple) before
|
||||
# entrypoint run `rustup default 1.100.0` (same musl host triple) before
|
||||
# cargo-deny touches the workspace, so the toolchain file resolves to an
|
||||
# already-installed toolchain on every run.
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
rust-version: stable
|
||||
rust-version: 1.100.0
|
||||
+14
-12
@@ -203,7 +203,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -428,7 +428,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# The GitHub Actions cache backend is main-only, mirroring
|
||||
@@ -487,7 +487,7 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
components: rustfmt, clippy
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
@@ -728,6 +728,8 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: "1.89"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
@@ -751,7 +753,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
@@ -825,11 +827,11 @@ jobs:
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- name: Install macOS LLVM tools
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
run: rustup component add llvm-tools --toolchain stable
|
||||
run: rustup component add llvm-tools
|
||||
- name: Omit macOS build debug metadata
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
@@ -1222,7 +1224,7 @@ jobs:
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/update-user-path.tests.ps1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
- name: Check every workspace target compiles on Windows
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
@@ -1267,7 +1269,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
@@ -1305,7 +1307,7 @@ jobs:
|
||||
run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context."
|
||||
- uses: actions/checkout@v7
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
@@ -1313,7 +1315,7 @@ jobs:
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' && matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
@@ -1364,7 +1366,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
@@ -1423,7 +1425,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- name: Install Linux system dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
components: rustfmt
|
||||
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
|
||||
@@ -67,13 +67,13 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y binutils musl-tools
|
||||
rustup target add --toolchain stable aarch64-unknown-linux-musl
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
shell: bash
|
||||
# Nightly artifacts are disposable smoke binaries (14-day retention),
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install Rust target
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@1.100.0
|
||||
with:
|
||||
targets: aarch64-unknown-linux-ohos
|
||||
- name: Install OpenHarmony native SDK
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: pet/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- run: npm --prefix pet ci --ignore-scripts
|
||||
- run: npm --prefix pet run check
|
||||
- run: npm --prefix pet run sync
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
cache-dependency-path: pet/package-lock.json
|
||||
- run: npm --prefix pet ci --ignore-scripts
|
||||
- run: npm --prefix pet run check
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: runner.os == 'macOS'
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
@@ -172,13 +172,13 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y binutils musl-tools
|
||||
rustup target add --toolchain stable ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }}
|
||||
cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
|
||||
- name: Configure Android NDK linker
|
||||
if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
package-manager-cache: false
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # stable 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
- name: Match dispatch to the requested commit
|
||||
id: source
|
||||
shell: bash
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
components: clippy, rustfmt
|
||||
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
id: sccache
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit
|
||||
- name: Run cargo audit
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.expected_sha }}
|
||||
fetch-depth: 1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Run focused tests
|
||||
shell: bash
|
||||
|
||||
Generated
-1
@@ -1179,7 +1179,6 @@ dependencies = [
|
||||
"agent-client-protocol",
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -3049,7 +3049,6 @@ fn start_cli_telemetry(
|
||||
telemetry::init(consent);
|
||||
telemetry::record(Event::SessionStart {
|
||||
source: SessionSource::Unknown,
|
||||
install_age: telemetry::install_age(),
|
||||
});
|
||||
Some(CliTelemetrySession {
|
||||
started: std::time::Instant::now(),
|
||||
|
||||
@@ -484,22 +484,6 @@ pub fn public_model_identifier(model: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Case-insensitive form of [`public_model_identifier`] for ids that were
|
||||
/// lowercased before the lookup.
|
||||
#[must_use]
|
||||
pub fn public_model_identifier_ignore_case(model: &str) -> bool {
|
||||
let catalog = super::bundled_models_dev_catalog();
|
||||
let matches = |id: &str| id.eq_ignore_ascii_case(model);
|
||||
catalog.reviewed.intrinsic.keys().any(|id| matches(id))
|
||||
|| catalog.models.keys().any(|id| matches(id))
|
||||
|| catalog.providers.values().any(|provider| {
|
||||
provider
|
||||
.models
|
||||
.iter()
|
||||
.any(|(id, row)| matches(id) || matches(&row.id))
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn official_deepseek_model_id(model: &str) -> Option<&'static str> {
|
||||
bundled_reviewed()
|
||||
|
||||
@@ -45,7 +45,7 @@ pub const SETUP_STATE_FILE_NAME: &str = "setup_state.json";
|
||||
/// when the collection policy, schema, or disclosure materially changes. Prior
|
||||
/// declines remain off. Keying it to the app version would re-prompt every
|
||||
/// release, which is nagging with extra steps.
|
||||
pub const TELEMETRY_NOTICE_VERSION: &str = "8";
|
||||
pub const TELEMETRY_NOTICE_VERSION: &str = "6";
|
||||
|
||||
/// Canonical setup step ids. The ordering matches the first-run spine so a
|
||||
/// `BTreeMap<SetupStep, _>` renders in wizard order.
|
||||
|
||||
@@ -219,9 +219,6 @@ libc = "0.2"
|
||||
windows-sys = { version = "0.61.2", features = ["Wdk_Storage_FileSystem", "Win32_System_IO", "Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Isolation", "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Pipes", "Win32_System_Threading", "Win32_System_SystemServices", "Win32_System_WindowsProgramming"] }
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows", all(target_os = "linux", not(target_env = "ohos"))))'.dependencies]
|
||||
arboard = { version = "3.4", features = ["wayland-data-control"] }
|
||||
|
||||
[target.'cfg(not(target_env = "ohos"))'.dependencies]
|
||||
portable-pty = "0.9"
|
||||
|
||||
|
||||
@@ -11793,7 +11793,6 @@ fn record_model_turn_telemetry(
|
||||
{
|
||||
return;
|
||||
}
|
||||
let model = turn.last_parent_model.as_deref().unwrap_or(model);
|
||||
let outcome = match status {
|
||||
TurnOutcomeStatus::Completed => codewhale_telemetry::TurnOutcome::Ok,
|
||||
TurnOutcomeStatus::Failed => codewhale_telemetry::TurnOutcome::Failed,
|
||||
|
||||
@@ -73,7 +73,6 @@ include!("tests/test_cases_18.rs");
|
||||
include!("tests/test_cases_19.rs");
|
||||
|
||||
mod admission_gates;
|
||||
mod model_turn_telemetry;
|
||||
mod redacted_thinking;
|
||||
mod runtime_state;
|
||||
mod sse_turn_recovery;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_turn_telemetry_names_the_model_the_last_request_used() {
|
||||
let mut turn = crate::core::turn::TurnContext::new(4);
|
||||
turn.add_parent_usage(&Usage {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
..Usage::default()
|
||||
});
|
||||
turn.note_parent_model("deepseek-v4pro");
|
||||
|
||||
record_model_turn_telemetry(
|
||||
codewhale_config::ProviderKind::Deepseek,
|
||||
"auto",
|
||||
&turn,
|
||||
TurnOutcomeStatus::Completed,
|
||||
);
|
||||
|
||||
let reported = codewhale_telemetry::session_counters()
|
||||
.usage_events()
|
||||
.into_iter()
|
||||
.any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
codewhale_telemetry::Event::ModelUsage { provider, model, .. }
|
||||
if provider == "deepseek" && model == "deepseek-v4-pro"
|
||||
)
|
||||
});
|
||||
assert!(reported, "the resolved wire model, not `auto`, is reported");
|
||||
}
|
||||
@@ -641,7 +641,6 @@ impl Engine {
|
||||
job.response_settled(!response_stream_failed);
|
||||
}
|
||||
turn.add_parent_usage(&usage);
|
||||
turn.note_parent_model(&stream_request.model);
|
||||
turn.note_parent_prompt_len(
|
||||
self.session.messages.len(),
|
||||
crate::compaction::estimate_request_input_tokens(
|
||||
|
||||
@@ -93,9 +93,6 @@ pub struct TurnContext {
|
||||
/// their own routed receipts.
|
||||
pub parent_route_usage: Usage,
|
||||
|
||||
/// Model the most recent parent-route request was dispatched with.
|
||||
pub last_parent_model: Option<String>,
|
||||
|
||||
/// Provider calls whose usage became ambiguous after dispatch (for
|
||||
/// example an RLM timeout). A non-zero value makes cost coverage
|
||||
/// explicitly incomplete instead of inventing a zero-usage receipt.
|
||||
@@ -202,7 +199,6 @@ impl TurnContext {
|
||||
..Usage::default()
|
||||
},
|
||||
parent_route_usage: Usage::default(),
|
||||
last_parent_model: None,
|
||||
routed_usage_dropped_records: 0,
|
||||
latest_parent_input_tokens: None,
|
||||
messages_len_at_last_parent_prompt: None,
|
||||
@@ -279,10 +275,6 @@ impl TurnContext {
|
||||
add_usage_to(&mut self.parent_route_usage, usage);
|
||||
}
|
||||
|
||||
pub fn note_parent_model(&mut self, model: &str) {
|
||||
self.last_parent_model = Some(model.to_string());
|
||||
}
|
||||
|
||||
pub fn add_routed_usage_dropped_records(&mut self, dropped_records: u64) {
|
||||
self.routed_usage_dropped_records = self
|
||||
.routed_usage_dropped_records
|
||||
|
||||
@@ -226,10 +226,10 @@
|
||||
"TranscriptReasoningExpand": "expand",
|
||||
"TranscriptThought": "Thought",
|
||||
"TranscriptThoughtFor": "Thought for {duration}",
|
||||
"TelemetryNoticeDefaultOn": "Usage reporting is on by default. Share aggregate version/platform, session, feature and error counts with Codewhale and PostHog (processor). Also counts providers, public catalog models, token totals, request counts, model wait time, prompt-size ranges, built-in tool use, how an exec session started and a coarse install-age range. No content or IP. docs/TELEMETRY.md Turn off: codewhale config set telemetry false",
|
||||
"TelemetryNoticeDefaultOn": "Usage reporting is on by default. Share aggregate version/platform, session, feature and error counts with Codewhale and PostHog (processor). Also counts providers, public catalog models, token totals and built-in tool use. No content or IP. docs/TELEMETRY.md Turn off: codewhale config set telemetry false",
|
||||
"TelemetryNoticeHeadline": "Codewhale usage reporting",
|
||||
"TelemetryNoticeBody": "Codewhale counts: which version you run, OS and CPU family, session\nduration and outcome, and aggregate feature and error counters.\n\nIt also counts the providers and public catalog models you use, their\ntoken totals, request counts, time spent waiting on the model and\nprompt-size ranges, how often each built-in tool runs, how an exec\nsession was started, and a coarse install-age range.\n\nIt never collects your conversations, code, prompts, files, repo or\nbranch names, model content, or credentials — and it never sends a\nper-turn or per-tool timeline of agent activity.\n\nYou are identified only by a random ID stored on this machine, replaced\nevery 90 days. Change your mind any time:\n codewhale config set telemetry false\n\nFull schema, field by field: docs/TELEMETRY.md\n\nUsage reporting is on by default. Codewhale and PostHog process these counts when delivery is configured. No IP is collected.",
|
||||
"TelemetryNoticeCompactBody": "Counts: version, OS/CPU, session\ntime/result, feature/error totals.\nAlso: provider/model, tokens, tools,\nrequests, wait time, prompt-size and\ninstall-age ranges, exec start.\nRandom local ID rotates every 90d.\nNever: chat/code/prompts/files/names;\nmodel content/credentials; turn/tools.\nSchema: docs/TELEMETRY.md\nOff: codewhale config set telemetry\nfalse\nUsage reporting is on by default. Codewhale and PostHog process these counts when delivery is configured. No IP is collected.",
|
||||
"TelemetryNoticeBody": "Codewhale counts: which version you run, OS and CPU family, session\nduration and outcome, and aggregate feature and error counters.\n\nIt also counts the providers and public catalog models you use, their\ntoken totals, and how often each built-in tool runs.\n\nIt never collects your conversations, code, prompts, files, repo or\nbranch names, model content, or credentials — and it never sends a\nper-turn or per-tool timeline of agent activity.\n\nYou are identified only by a random ID stored on this machine, replaced\nevery 90 days. Change your mind any time:\n codewhale config set telemetry false\n\nFull schema, field by field: docs/TELEMETRY.md\n\nUsage reporting is on by default. Codewhale and PostHog process these counts when delivery is configured. No IP is collected.",
|
||||
"TelemetryNoticeCompactBody": "Counts: version, OS/CPU, session\ntime/result, feature/error totals.\nAlso: provider/model, tokens, tools.\nRandom local ID rotates every 90d.\nNever: chat/code/prompts/files/names;\nmodel content/credentials; turn/tools.\nSchema: docs/TELEMETRY.md\nOff: codewhale config set telemetry\nfalse\nUsage reporting is on by default. Codewhale and PostHog process these counts when delivery is configured. No IP is collected.",
|
||||
"TelemetryNoticeChoiceKeep": "Yes, keep anonymous counts",
|
||||
"TelemetryNoticeChoiceDisable": "No, turn off tracking",
|
||||
"TelemetryNoticeActionChoose": "choose",
|
||||
|
||||
@@ -344,7 +344,6 @@ mod tests {
|
||||
};
|
||||
handle.record(Event::SessionStart {
|
||||
source: crate::SessionSource::Unknown,
|
||||
install_age: None,
|
||||
});
|
||||
let waiter = std::thread::spawn(move || handle.persist_local());
|
||||
assert!(matches!(rx.try_recv(), Ok(Message::Event(_))));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::event::{ContextSize, Counters, Errors, Event, ModelCounters, TurnWall};
|
||||
use crate::event::{Counters, Errors, Event, ModelCounters, TurnWall};
|
||||
|
||||
/// One session's accumulated counts.
|
||||
#[derive(Debug, Default)]
|
||||
@@ -48,7 +48,6 @@ struct ModelSlot {
|
||||
model: String,
|
||||
counters: ModelCounters,
|
||||
turn_wall: TurnWall,
|
||||
context: ContextSize,
|
||||
}
|
||||
|
||||
/// At most this many distinct provider and model pairs per session; later
|
||||
@@ -187,7 +186,6 @@ impl SessionCounters {
|
||||
model,
|
||||
counters: ModelCounters::default(),
|
||||
turn_wall: TurnWall::default(),
|
||||
context: ContextSize::default(),
|
||||
});
|
||||
models.len() - 1
|
||||
}
|
||||
@@ -240,7 +238,6 @@ impl SessionCounters {
|
||||
model: slot.model.clone(),
|
||||
counters: slot.counters,
|
||||
turn_wall: slot.turn_wall,
|
||||
context: slot.context,
|
||||
}));
|
||||
}
|
||||
if let Ok(tools) = self.tools.lock() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::buffer;
|
||||
use crate::event::{Arch, InstallAge, Libc, Os};
|
||||
use crate::event::{Arch, Libc, Os};
|
||||
|
||||
/// How long an install id may live before it is replaced.
|
||||
///
|
||||
@@ -47,77 +47,6 @@ pub struct TelemetryState {
|
||||
/// a permanently offline machine tries at most once per interval.
|
||||
#[serde(default)]
|
||||
pub last_flush: Option<String>,
|
||||
/// The UTC day (`YYYY-MM-DD`) this state was first created. Local only: it
|
||||
/// is read to pick an [`InstallAge`] bucket and is never sent.
|
||||
#[serde(default)]
|
||||
pub first_seen_day: Option<String>,
|
||||
/// Set when this file predates `first_seen_day`, so its age is unknown and
|
||||
/// is reported as the oldest bucket rather than as new.
|
||||
#[serde(default)]
|
||||
pub first_seen_unknown: bool,
|
||||
}
|
||||
|
||||
enum StateRead {
|
||||
Missing,
|
||||
Unreadable,
|
||||
Present(TelemetryState),
|
||||
}
|
||||
|
||||
fn read_state_checked(root: &Path) -> StateRead {
|
||||
match std::fs::read_to_string(buffer::state_path(root)) {
|
||||
Ok(body) => serde_json::from_str::<TelemetryState>(&body)
|
||||
.map_or(StateRead::Unreadable, StateRead::Present),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => StateRead::Missing,
|
||||
Err(_) => StateRead::Unreadable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Today's date in UTC.
|
||||
#[must_use]
|
||||
pub fn utc_today() -> chrono::NaiveDate {
|
||||
chrono::Utc::now().date_naive()
|
||||
}
|
||||
|
||||
/// Record the first-seen day if this state has none. A missing state file is a
|
||||
/// new install; a file that has a recorded version but no day predates this
|
||||
/// field and is marked unknown. An unreadable file is left untouched.
|
||||
pub(crate) fn stamp_first_seen(root: &Path, today: chrono::NaiveDate) {
|
||||
let mut state = match read_state_checked(root) {
|
||||
StateRead::Unreadable => return,
|
||||
StateRead::Missing => TelemetryState::default(),
|
||||
StateRead::Present(state) => state,
|
||||
};
|
||||
if state.first_seen_day.is_some() || state.first_seen_unknown {
|
||||
return;
|
||||
}
|
||||
if state.last_version.is_some() {
|
||||
state.first_seen_unknown = true;
|
||||
} else {
|
||||
state.first_seen_day = Some(today.format("%Y-%m-%d").to_string());
|
||||
}
|
||||
let _ = write_state(root, &state);
|
||||
}
|
||||
|
||||
/// The coarse age bucket for this installation, or `None` when the local state
|
||||
/// cannot be read. A missing state is new.
|
||||
pub(crate) fn install_age_at(root: &Path, today: chrono::NaiveDate) -> Option<InstallAge> {
|
||||
let state = match read_state_checked(root) {
|
||||
StateRead::Unreadable => return None,
|
||||
StateRead::Missing => return Some(InstallAge::NewToday),
|
||||
StateRead::Present(state) => state,
|
||||
};
|
||||
if state.first_seen_unknown {
|
||||
return Some(InstallAge::Older);
|
||||
}
|
||||
let Some(day) = state.first_seen_day else {
|
||||
return Some(InstallAge::NewToday);
|
||||
};
|
||||
let first = chrono::NaiveDate::parse_from_str(&day, "%Y-%m-%d").ok()?;
|
||||
Some(match today.signed_duration_since(first).num_days() {
|
||||
..=0 => InstallAge::NewToday,
|
||||
1..=6 => InstallAge::UnderWeek,
|
||||
_ => InstallAge::Older,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read the install id, minting a fresh one if it is missing, unreadable,
|
||||
|
||||
+26
-122
@@ -33,10 +33,10 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Wire schema version. Bumped on any field add, remove, or retype; never
|
||||
/// reused. `crates/telemetry/tests/golden/v1.json` pins what v1 was.
|
||||
pub const SCHEMA_VERSION: u32 = 6;
|
||||
pub const SCHEMA_VERSION: u32 = 4;
|
||||
|
||||
/// Default-on policy disclosure version. This is not human consent.
|
||||
pub const NOTICE_VERSION: u32 = 8;
|
||||
pub const NOTICE_VERSION: u32 = 6;
|
||||
|
||||
/// Which product surface produced a batch.
|
||||
///
|
||||
@@ -277,12 +277,6 @@ pub enum SessionSource {
|
||||
/// Started under CI (`CI` or `GITHUB_ACTIONS` set), including the GitHub
|
||||
/// Action, so automation is never counted as a person.
|
||||
Ci,
|
||||
/// `exec` started by Codewhale's own fleet dispatcher.
|
||||
FleetWorker,
|
||||
/// `exec` started from inside an agent's shell command.
|
||||
Agent,
|
||||
/// `exec` whose stdin or stdout is not a terminal.
|
||||
Scripted,
|
||||
}
|
||||
|
||||
impl SessionSource {
|
||||
@@ -294,9 +288,6 @@ impl SessionSource {
|
||||
Self::Api,
|
||||
Self::Unknown,
|
||||
Self::Ci,
|
||||
Self::FleetWorker,
|
||||
Self::Agent,
|
||||
Self::Scripted,
|
||||
];
|
||||
|
||||
/// The wire spelling.
|
||||
@@ -309,37 +300,6 @@ impl SessionSource {
|
||||
Self::Api => "api",
|
||||
Self::Unknown => "unknown",
|
||||
Self::Ci => "ci",
|
||||
Self::FleetWorker => "fleet_worker",
|
||||
Self::Agent => "agent",
|
||||
Self::Scripted => "scripted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How long this install has existed, as a coarse bucket. The first-seen date
|
||||
/// itself never leaves the device.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InstallAge {
|
||||
/// First seen on the current UTC day.
|
||||
NewToday,
|
||||
/// First seen 1 to 6 UTC days ago.
|
||||
UnderWeek,
|
||||
/// First seen 7 or more UTC days ago, or before the date was recorded.
|
||||
Older,
|
||||
}
|
||||
|
||||
impl InstallAge {
|
||||
/// Every value, for exhaustive iteration.
|
||||
pub const ALL: &'static [Self] = &[Self::NewToday, Self::UnderWeek, Self::Older];
|
||||
|
||||
/// The wire spelling.
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::NewToday => "new_today",
|
||||
Self::UnderWeek => "under_week",
|
||||
Self::Older => "older",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,20 +366,11 @@ pub enum ExitClass {
|
||||
Panic,
|
||||
/// Exited non-successfully without a signal or a panic.
|
||||
Error,
|
||||
/// The process ended without its shutdown report; its aggregates were
|
||||
/// recovered from a local checkpoint at a later start.
|
||||
Unclean,
|
||||
}
|
||||
|
||||
impl ExitClass {
|
||||
/// Every value, for exhaustive iteration.
|
||||
pub const ALL: &'static [Self] = &[
|
||||
Self::Clean,
|
||||
Self::Signal,
|
||||
Self::Panic,
|
||||
Self::Error,
|
||||
Self::Unclean,
|
||||
];
|
||||
pub const ALL: &'static [Self] = &[Self::Clean, Self::Signal, Self::Panic, Self::Error];
|
||||
|
||||
/// The wire spelling.
|
||||
#[must_use]
|
||||
@@ -429,7 +380,6 @@ impl ExitClass {
|
||||
Self::Signal => "signal",
|
||||
Self::Panic => "panic",
|
||||
Self::Error => "error",
|
||||
Self::Unclean => "unclean",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +391,6 @@ impl ExitClass {
|
||||
Self::Signal => 1,
|
||||
Self::Panic => 2,
|
||||
Self::Error => 3,
|
||||
Self::Unclean => 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +401,6 @@ impl ExitClass {
|
||||
1 => Self::Signal,
|
||||
2 => Self::Panic,
|
||||
3 => Self::Error,
|
||||
4 => Self::Unclean,
|
||||
_ => Self::Clean,
|
||||
}
|
||||
}
|
||||
@@ -691,9 +639,6 @@ pub enum Event {
|
||||
SessionStart {
|
||||
/// How it was started.
|
||||
source: SessionSource,
|
||||
/// Coarse install age. `null` when the local state cannot be read.
|
||||
#[serde(default)]
|
||||
install_age: Option<InstallAge>,
|
||||
},
|
||||
/// A session ended. Everything the session accumulated ships here, once.
|
||||
SessionEnd {
|
||||
@@ -751,9 +696,6 @@ pub enum Event {
|
||||
counters: ModelCounters,
|
||||
/// Turn wall-clock buckets for these turns.
|
||||
turn_wall: TurnWall,
|
||||
/// Requests by input size.
|
||||
#[serde(default)]
|
||||
context: ContextSize,
|
||||
},
|
||||
/// One session's calls of one tool.
|
||||
ToolUsage {
|
||||
@@ -783,16 +725,6 @@ pub struct ModelCounters {
|
||||
pub cache_read_tokens: u64,
|
||||
/// Input tokens written to the provider's prompt cache.
|
||||
pub cache_write_tokens: u64,
|
||||
/// Provider requests on the session's own route for this model.
|
||||
#[serde(default)]
|
||||
pub requests: u32,
|
||||
/// Reasoning tokens as the provider reports them; a subset of
|
||||
/// `output_tokens`, which is not reduced.
|
||||
#[serde(default)]
|
||||
pub reasoning_tokens: u64,
|
||||
/// Whole seconds spent waiting on the model, summed over the session.
|
||||
#[serde(default)]
|
||||
pub generation_secs: u32,
|
||||
}
|
||||
|
||||
impl ModelCounters {
|
||||
@@ -805,44 +737,9 @@ impl ModelCounters {
|
||||
"output_tokens",
|
||||
"cache_read_tokens",
|
||||
"cache_write_tokens",
|
||||
"requests",
|
||||
"reasoning_tokens",
|
||||
"generation_secs",
|
||||
];
|
||||
}
|
||||
|
||||
/// Per-session count of model requests by input size, in tokens including
|
||||
/// cache reads. Half-open intervals; K is 1024. A histogram, never a
|
||||
/// per-request series.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ContextSize {
|
||||
/// Requests with `[0, 32768)` input tokens.
|
||||
pub lt_32k: u32,
|
||||
/// Requests with `[32768, 131072)` input tokens.
|
||||
#[serde(rename = "32k_128k")]
|
||||
pub k32_to_k128: u32,
|
||||
/// Requests with `[131072, 262144)` input tokens.
|
||||
#[serde(rename = "128k_256k")]
|
||||
pub k128_to_k256: u32,
|
||||
/// Requests with 262144 or more input tokens.
|
||||
pub gte_256k: u32,
|
||||
}
|
||||
|
||||
impl ContextSize {
|
||||
/// Field names in wire spelling, in declaration order.
|
||||
pub const FIELDS: &'static [&'static str] = &["lt_32k", "32k_128k", "128k_256k", "gte_256k"];
|
||||
|
||||
/// Record one request with `tokens` input tokens.
|
||||
pub fn observe(&mut self, tokens: u64) {
|
||||
match tokens {
|
||||
0..32_768 => self.lt_32k = self.lt_32k.saturating_add(1),
|
||||
32_768..131_072 => self.k32_to_k128 = self.k32_to_k128.saturating_add(1),
|
||||
131_072..262_144 => self.k128_to_k256 = self.k128_to_k256.saturating_add(1),
|
||||
_ => self.gte_256k = self.gte_256k.saturating_add(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// First-party tool names reported by `tool_usage`. Any other name is
|
||||
/// reported as `mcp` or `other`, so a plugin's or server's tool name never
|
||||
/// leaves the machine.
|
||||
@@ -933,22 +830,17 @@ pub fn is_reported_tool_name(value: &str) -> bool {
|
||||
value == "mcp" || value == "other" || TOOL_NAMES.contains(&value)
|
||||
}
|
||||
|
||||
/// The `model` value for a model id: the lowercased canonical id when the
|
||||
/// bundled model catalog lists it, otherwise `other`. Official aliases and
|
||||
/// compatibility spellings are resolved first. Custom, local and fine-tuned
|
||||
/// model names therefore never leave the machine.
|
||||
/// The `model` value for a model id: the lowercased id when the bundled model
|
||||
/// catalog lists it, otherwise `other`. Custom, local and fine-tuned model
|
||||
/// names therefore never leave the machine.
|
||||
#[must_use]
|
||||
pub fn reported_model_id(model: &str) -> String {
|
||||
use codewhale_config::catalog::reviewed::{compatibility_alias, official_deepseek_model_id};
|
||||
let id = model.trim().to_ascii_lowercase();
|
||||
let canonical = official_deepseek_model_id(&id)
|
||||
.or_else(|| compatibility_alias("canonical_model_name", &id))
|
||||
.map(str::to_ascii_lowercase);
|
||||
canonical
|
||||
.into_iter()
|
||||
.chain(std::iter::once(id))
|
||||
.find(|candidate| is_catalogued_model(candidate))
|
||||
.unwrap_or_else(|| "other".to_string())
|
||||
if is_catalogued_model(&id) {
|
||||
id
|
||||
} else {
|
||||
"other".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `value` may appear as `model_usage.model`.
|
||||
@@ -958,9 +850,21 @@ pub fn is_reported_model_id(value: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_catalogued_model(id: &str) -> bool {
|
||||
!id.is_empty()
|
||||
&& id.len() <= 128
|
||||
&& codewhale_config::catalog::reviewed::public_model_identifier_ignore_case(id)
|
||||
if id.is_empty() || id.len() > 128 {
|
||||
return false;
|
||||
}
|
||||
let catalog = codewhale_config::catalog::bundled_models_dev_catalog();
|
||||
catalog.reviewed.intrinsic.contains_key(id)
|
||||
|| catalog
|
||||
.models
|
||||
.keys()
|
||||
.any(|key| key.eq_ignore_ascii_case(id))
|
||||
|| catalog.providers.values().any(|provider| {
|
||||
provider
|
||||
.models
|
||||
.keys()
|
||||
.any(|key| key.eq_ignore_ascii_case(id))
|
||||
})
|
||||
}
|
||||
|
||||
impl Event {
|
||||
|
||||
@@ -57,9 +57,9 @@ pub use decision::{
|
||||
};
|
||||
pub use envelope::reduce_panic_site;
|
||||
pub use event::{
|
||||
Arch, Batch, ColdStartBucket, ContextSize, Counters, DurationBucket, Errors, Event, ExitClass,
|
||||
InstallAge, InstallKind, Libc, ModelCounters, NOTICE_VERSION, Os, ProductCounters,
|
||||
SCHEMA_VERSION, SessionSource, Surface, TurnWall,
|
||||
Arch, Batch, ColdStartBucket, Counters, DurationBucket, Errors, Event, ExitClass, InstallKind,
|
||||
Libc, ModelCounters, NOTICE_VERSION, Os, ProductCounters, SCHEMA_VERSION, SessionSource,
|
||||
Surface, TurnWall,
|
||||
};
|
||||
|
||||
/// How long the shutdown flush may hold the process.
|
||||
@@ -123,18 +123,9 @@ pub fn init(consent: TelemetryConsent) {
|
||||
exit_class: AtomicU8::new(ExitClass::Clean.as_u8()),
|
||||
});
|
||||
|
||||
envelope::stamp_first_seen(&root, envelope::utc_today());
|
||||
record_install_or_upgrade(&root);
|
||||
}
|
||||
|
||||
/// The coarse install-age bucket for this process's `session_start`, or `None`
|
||||
/// when unarmed or when the local state cannot be read.
|
||||
#[must_use]
|
||||
pub fn install_age() -> Option<InstallAge> {
|
||||
let armed = ARMED.get()?;
|
||||
envelope::install_age_at(&armed.root, envelope::utc_today())
|
||||
}
|
||||
|
||||
/// Note that this binary's version differs from the one last seen on this
|
||||
/// machine, at most once per version.
|
||||
///
|
||||
|
||||
@@ -25,9 +25,7 @@ Codewhale counts: which version you run, OS and CPU family, session
|
||||
duration and outcome, and aggregate feature and error counters.
|
||||
|
||||
It also counts the providers and public catalog models you use, their
|
||||
token totals, request counts, time spent waiting on the model and
|
||||
prompt-size ranges, how often each built-in tool runs, how an exec
|
||||
session was started, and a coarse install-age range.
|
||||
token totals, and how often each built-in tool runs.
|
||||
|
||||
It never collects your conversations, code, prompts, files, repo or
|
||||
branch names, model content, or credentials — and it never sends a
|
||||
@@ -43,7 +41,7 @@ Usage reporting is on by default. Codewhale and PostHog process these
|
||||
counts when delivery is configured. No IP is collected.";
|
||||
|
||||
/// Concise disclosure for every armed runtime surface, including headless CLI.
|
||||
pub const STARTUP_DISCLOSURE: &str = "Usage reporting is on by default: Codewhale and PostHog process aggregate version/platform, session, feature, error, model/token/request/wait-time/prompt-size, exec-start, install-age range and built-in tool counts when delivery is configured. No content or IP. Turn off: codewhale config set telemetry false. Details: codewhale config telemetry";
|
||||
pub const STARTUP_DISCLOSURE: &str = "Usage reporting is on by default: Codewhale and PostHog process aggregate version/platform, session, feature, error, model/token and built-in tool counts when delivery is configured. No content or IP. Turn off: codewhale config set telemetry false. Details: codewhale config telemetry";
|
||||
|
||||
/// Present the policy once per revision without recording human acceptance.
|
||||
/// Failure to save the display marker only causes a later repeat disclosure.
|
||||
|
||||
+11
-322
@@ -121,12 +121,11 @@ fn every_event() -> Vec<Event> {
|
||||
previous_version: Some("0.9.3-rc.1".to_string()),
|
||||
},
|
||||
Event::SessionStart {
|
||||
source: SessionSource::FleetWorker,
|
||||
install_age: Some(InstallAge::UnderWeek),
|
||||
source: SessionSource::Resume,
|
||||
},
|
||||
Event::SessionEnd {
|
||||
duration_bucket: DurationBucket::OneToTen,
|
||||
exit_class: ExitClass::Unclean,
|
||||
exit_class: ExitClass::Panic,
|
||||
cold_start_bucket: Some(ColdStartBucket::Mid),
|
||||
providers: vec!["custom".to_string(), "deepseek".to_string()],
|
||||
counters: Counters {
|
||||
@@ -184,9 +183,6 @@ fn every_event() -> Vec<Event> {
|
||||
output_tokens: 9_120,
|
||||
cache_read_tokens: 380_001,
|
||||
cache_write_tokens: 0,
|
||||
requests: 27,
|
||||
reasoning_tokens: 2_310,
|
||||
generation_secs: 412,
|
||||
},
|
||||
turn_wall: TurnWall {
|
||||
lt_5s: 3,
|
||||
@@ -194,12 +190,6 @@ fn every_event() -> Vec<Event> {
|
||||
thirty_to_onetwenty: 2,
|
||||
gte_120s: 0,
|
||||
},
|
||||
context: ContextSize {
|
||||
lt_32k: 9,
|
||||
k32_to_k128: 11,
|
||||
k128_to_k256: 5,
|
||||
gte_256k: 2,
|
||||
},
|
||||
},
|
||||
Event::ToolUsage {
|
||||
tool: "read_file".to_string(),
|
||||
@@ -279,7 +269,6 @@ fn closed_enum_values() -> Vec<String> {
|
||||
values.extend(Libc::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(InstallKind::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(SessionSource::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(InstallAge::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(DurationBucket::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(ExitClass::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
values.extend(ColdStartBucket::ALL.iter().map(|v| v.as_str().to_string()));
|
||||
@@ -1237,7 +1226,6 @@ fn probe_threshold_cannot_hide_an_over_cap_buffer() {
|
||||
// only safe if `MAX_EVENTS` lines cannot fit under it.
|
||||
let shortest = serde_json::to_string(&Event::SessionStart {
|
||||
source: SessionSource::Api,
|
||||
install_age: None,
|
||||
})
|
||||
.expect("serialize");
|
||||
let floor = (shortest.len() as u64 + 1) * buffer::MAX_EVENTS as u64;
|
||||
@@ -1458,7 +1446,6 @@ fn record_blocking_is_a_noop_when_unarmed() {
|
||||
});
|
||||
crate::record(Event::SessionStart {
|
||||
source: SessionSource::Interactive,
|
||||
install_age: None,
|
||||
});
|
||||
crate::set_exit_class(ExitClass::Panic);
|
||||
assert_eq!(crate::exit_class(), ExitClass::Clean);
|
||||
@@ -1672,7 +1659,6 @@ fn no_public_api_accepts_a_bare_bool() {
|
||||
|
||||
const TELEMETRY_DOC: &str = include_str!("../../../docs/TELEMETRY.md");
|
||||
const GOLDEN_V4: &str = include_str!("../tests/golden/v4.json");
|
||||
const GOLDEN_V6: &str = include_str!("../tests/golden/v6.json");
|
||||
|
||||
/// Extract the fenced ```jsonc blocks from the schema doc, in order.
|
||||
fn jsonc_blocks(doc: &str) -> Vec<String> {
|
||||
@@ -1878,12 +1864,13 @@ fn event_field_names_match_documented_schema() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_payload_v6() {
|
||||
fn golden_payload_v4() {
|
||||
assert_eq!(NOTICE_VERSION.to_string(), TELEMETRY_NOTICE_VERSION);
|
||||
// `crates/telemetry/tests/golden/v6.json` is one fully-populated instance of
|
||||
// the envelope and every event, and the batch the collector's own tests
|
||||
// post. Any field add, remove, or retype fails here until the developer
|
||||
// re-blesses it under a bumped `SCHEMA_VERSION`.
|
||||
// `crates/telemetry/tests/golden/v4.json` is one fully-populated instance of
|
||||
// the envelope and every event. Any field add, remove, or retype fails here
|
||||
// until the developer re-blesses it under a bumped `SCHEMA_VERSION` — and it
|
||||
// is also the artifact a future receiver author reads to know exactly what
|
||||
// v1 was.
|
||||
//
|
||||
// Re-bless with: `CODEWHALE_BLESS_TELEMETRY_GOLDEN=1 cargo test -p codewhale-telemetry`
|
||||
let batch = every_field_batch();
|
||||
@@ -1891,81 +1878,22 @@ fn golden_payload_v6() {
|
||||
batch.schema_version, SCHEMA_VERSION,
|
||||
"the fixture must be built at the current schema version"
|
||||
);
|
||||
assert_eq!(SCHEMA_VERSION, 6);
|
||||
let mut rendered = serde_json::to_string_pretty(&batch).expect("serialize");
|
||||
rendered.push('\n');
|
||||
|
||||
if std::env::var("CODEWHALE_BLESS_TELEMETRY_GOLDEN").is_ok() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/v6.json");
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/v4.json");
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("create golden dir");
|
||||
std::fs::write(&path, &rendered).expect("write golden");
|
||||
return;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
rendered, GOLDEN_V6,
|
||||
"the v6 payload changed; bump SCHEMA_VERSION and re-bless the golden file"
|
||||
rendered, GOLDEN_V4,
|
||||
"the v4 payload changed; bump SCHEMA_VERSION and re-bless the golden file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_schema_4_golden_still_reads_back_with_the_new_fields_defaulted() {
|
||||
let legacy: Batch = serde_json::from_str(GOLDEN_V4).expect("v4 golden parses");
|
||||
assert_eq!(legacy.schema_version, 4);
|
||||
for event in &legacy.events {
|
||||
match event {
|
||||
Event::SessionStart { install_age, .. } => assert_eq!(*install_age, None),
|
||||
Event::ModelUsage {
|
||||
counters, context, ..
|
||||
} => {
|
||||
assert_eq!(counters.requests, 0);
|
||||
assert_eq!(counters.reasoning_tokens, 0);
|
||||
assert_eq!(counters.generation_secs, 0);
|
||||
assert_eq!(*context, ContextSize::default());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_buckets_are_half_open_at_32k_128k_and_256k() {
|
||||
let mut size = ContextSize::default();
|
||||
for tokens in [
|
||||
0,
|
||||
32_767,
|
||||
32_768,
|
||||
131_071,
|
||||
131_072,
|
||||
262_143,
|
||||
262_144,
|
||||
u64::MAX,
|
||||
] {
|
||||
size.observe(tokens);
|
||||
}
|
||||
assert_eq!(
|
||||
size,
|
||||
ContextSize {
|
||||
lt_32k: 2,
|
||||
k32_to_k128: 2,
|
||||
k128_to_k256: 2,
|
||||
gte_256k: 2,
|
||||
}
|
||||
);
|
||||
let json = serde_json::to_value(size).expect("serialize");
|
||||
let keys: Vec<&str> = json
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
let mut expected: Vec<&str> = ContextSize::FIELDS.to_vec();
|
||||
expected.sort_unstable();
|
||||
let mut got = keys;
|
||||
got.sort_unstable();
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_comparison_names_install_upgrade_and_downgrade() {
|
||||
assert!(crate::version_is_older("0.9.3", "0.9.4"));
|
||||
@@ -2066,242 +1994,3 @@ fn the_notice_summarizes_what_the_schema_collects_and_states_every_red_line() {
|
||||
assert!(!body.contains("CODEWHALE_TELEMETRY=0"));
|
||||
assert!(body.contains("docs/TELEMETRY.md"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- model naming --
|
||||
|
||||
fn model_id_is_collector_shaped(id: &str) -> bool {
|
||||
let bytes = id.as_bytes();
|
||||
(1..=128).contains(&bytes.len())
|
||||
&& bytes[0].is_ascii_lowercase() | bytes[0].is_ascii_digit()
|
||||
&& bytes[1..].iter().all(|b| {
|
||||
b.is_ascii_lowercase()
|
||||
|| b.is_ascii_digit()
|
||||
|| matches!(b, b'.' | b'_' | b':' | b'/' | b'@' | b'+' | b'-')
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reported_model_id_resolves_aliases_and_spellings() {
|
||||
assert_eq!(reported_model_id("deepseek-v4-pro"), "deepseek-v4-pro");
|
||||
assert_eq!(reported_model_id(" DeepSeek-V4-Pro "), "deepseek-v4-pro");
|
||||
assert_eq!(reported_model_id("deepseek-v4pro"), "deepseek-v4-pro");
|
||||
assert_eq!(
|
||||
reported_model_id("deepseek-ai/deepseek-v4-pro"),
|
||||
"deepseek-v4-pro"
|
||||
);
|
||||
assert_eq!(
|
||||
reported_model_id("deepseek/deepseek-v4pro"),
|
||||
"deepseek-v4-pro"
|
||||
);
|
||||
assert_eq!(reported_model_id("pro"), "deepseek-v4-pro");
|
||||
assert_eq!(reported_model_id("flash"), "deepseek-flash");
|
||||
assert_eq!(reported_model_id("auto"), "other");
|
||||
assert_eq!(reported_model_id("my-private-finetune"), "other");
|
||||
assert_eq!(reported_model_id(""), "other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reported_model_id_never_leaves_the_catalog() {
|
||||
use codewhale_config::catalog::reviewed::{
|
||||
bundled_reviewed, public_model_identifier_ignore_case,
|
||||
};
|
||||
let reviewed = bundled_reviewed();
|
||||
let mut spellings: Vec<&str> = reviewed
|
||||
.official_deepseek_aliases
|
||||
.keys()
|
||||
.chain(reviewed.go_aliases.keys())
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
if let Some(group) = reviewed.compatibility_aliases.get("canonical_model_name") {
|
||||
spellings.extend(group.keys().map(String::as_str));
|
||||
}
|
||||
assert!(spellings.len() > 20, "alias tables look empty");
|
||||
for spelling in spellings {
|
||||
let reported = reported_model_id(spelling);
|
||||
assert!(is_reported_model_id(&reported), "{spelling} -> {reported}");
|
||||
assert!(
|
||||
model_id_is_collector_shaped(&reported),
|
||||
"{spelling} -> {reported}"
|
||||
);
|
||||
}
|
||||
for hostile in ["", "x/../y", "deepseek-v4-pro\n", "Ünï", &"a".repeat(300)] {
|
||||
let reported = reported_model_id(hostile);
|
||||
assert!(
|
||||
reported == "other" || public_model_identifier_ignore_case(&reported),
|
||||
"{hostile:?} -> {reported}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_listed_model_and_provider_default_maps_to_a_catalogued_id() {
|
||||
let catalog = codewhale_config::catalog::bundled_models_dev_catalog();
|
||||
let mut missing = Vec::new();
|
||||
let mut check = |origin: &str, id: &str| {
|
||||
let reported = reported_model_id(id);
|
||||
if reported == "other" || !model_id_is_collector_shaped(&reported) {
|
||||
missing.push(format!("{origin}: {id}"));
|
||||
}
|
||||
};
|
||||
for provider in catalog.providers.values() {
|
||||
for (id, row) in &provider.models {
|
||||
check("catalog", id);
|
||||
check("catalog row", &row.id);
|
||||
}
|
||||
}
|
||||
for id in catalog.models.keys() {
|
||||
check("canonical", id);
|
||||
}
|
||||
|
||||
let descriptors: Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../config/assets/provider_descriptors.json"),
|
||||
)
|
||||
.expect("provider descriptors"),
|
||||
)
|
||||
.expect("descriptors json");
|
||||
let exceptions: &[&str] = &["antigravity", "custom", "openai-compatible", "ollama"];
|
||||
for provider in descriptors["providers"].as_array().expect("providers") {
|
||||
let id = provider["id"].as_str().expect("provider id");
|
||||
let selectable = provider["selectable"].as_bool().unwrap_or(false);
|
||||
let default = provider["default_model"].as_str().unwrap_or_default();
|
||||
if selectable && !default.is_empty() && !exceptions.contains(&id) {
|
||||
check(&format!("default for {id}"), default);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"ids the catalog does not name:\n{}",
|
||||
missing.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- install age --
|
||||
|
||||
fn day(text: &str) -> chrono::NaiveDate {
|
||||
chrono::NaiveDate::parse_from_str(text, "%Y-%m-%d").expect("date")
|
||||
}
|
||||
|
||||
fn root_with_state(state: &str) -> (tempfile::TempDir, PathBuf) {
|
||||
let home = temp_home();
|
||||
let root = root_of(&home);
|
||||
buffer::ensure_dir(&root).expect("create telemetry dir");
|
||||
std::fs::write(buffer::state_path(&root), state).expect("write state");
|
||||
(home, root)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fresh_home_is_new_today_and_ages_by_utc_day() {
|
||||
let home = temp_home();
|
||||
let root = root_of(&home);
|
||||
buffer::ensure_dir(&root).expect("create telemetry dir");
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day("2026-11-13")),
|
||||
Some(InstallAge::NewToday)
|
||||
);
|
||||
envelope::stamp_first_seen(&root, day("2026-11-13"));
|
||||
for (today, expected) in [
|
||||
("2026-11-13", InstallAge::NewToday),
|
||||
("2026-11-14", InstallAge::UnderWeek),
|
||||
("2026-11-16", InstallAge::UnderWeek),
|
||||
("2026-11-19", InstallAge::UnderWeek),
|
||||
("2026-11-20", InstallAge::Older),
|
||||
("2027-06-01", InstallAge::Older),
|
||||
("2026-11-12", InstallAge::NewToday),
|
||||
] {
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day(today)),
|
||||
Some(expected),
|
||||
"{today}"
|
||||
);
|
||||
}
|
||||
envelope::stamp_first_seen(&root, day("2026-12-25"));
|
||||
assert_eq!(
|
||||
envelope::read_state(&root).first_seen_day.as_deref(),
|
||||
Some("2026-11-13"),
|
||||
"a second stamp never moves the first-seen day"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_state_written_before_the_day_existed_reads_older_never_new() {
|
||||
let (_home, root) = root_with_state(r#"{"schema_version":4,"last_version":"0.10.2"}"#);
|
||||
envelope::stamp_first_seen(&root, day("2026-11-13"));
|
||||
let state = envelope::read_state(&root);
|
||||
assert_eq!(state.first_seen_day, None);
|
||||
assert!(state.first_seen_unknown);
|
||||
assert_eq!(state.last_version.as_deref(), Some("0.10.2"));
|
||||
for today in ["2026-11-13", "2026-11-14", "2030-01-01"] {
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day(today)),
|
||||
Some(InstallAge::Older),
|
||||
"{today}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreadable_or_hostile_state_gives_null_and_is_not_overwritten() {
|
||||
for body in [
|
||||
"not json",
|
||||
r#"{"first_seen_day":"9999-99-99"}"#,
|
||||
r#"{"first_seen_day":"yesterday"}"#,
|
||||
] {
|
||||
let (_home, root) = root_with_state(body);
|
||||
envelope::stamp_first_seen(&root, day("2026-11-13"));
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day("2026-11-13")),
|
||||
None,
|
||||
"{body}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(buffer::state_path(&root)).expect("state"),
|
||||
body,
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opting_out_erases_the_age_and_rotating_the_install_id_keeps_it() {
|
||||
let home = temp_home();
|
||||
let root = root_of(&home);
|
||||
buffer::ensure_dir(&root).expect("create telemetry dir");
|
||||
envelope::stamp_first_seen(&root, day("2026-01-01"));
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day("2026-11-13")),
|
||||
Some(InstallAge::Older)
|
||||
);
|
||||
|
||||
std::fs::write(
|
||||
buffer::install_id_path(&root),
|
||||
r#"{"schema_version":1,"install_id":"3f2a9c1e-0000-4000-8000-000000000001","rotated_at":"2020-01-01T00:00:00Z"}"#,
|
||||
)
|
||||
.expect("write expired id");
|
||||
let rotated = envelope::read_or_create_install_id(&root).expect("rotate");
|
||||
assert_ne!(rotated.install_id, "3f2a9c1e-0000-4000-8000-000000000001");
|
||||
assert_eq!(
|
||||
envelope::read_state(&root).first_seen_day.as_deref(),
|
||||
Some("2026-01-01"),
|
||||
"rotation does not touch the age"
|
||||
);
|
||||
|
||||
buffer::wipe(&root).expect("wipe");
|
||||
std::fs::remove_file(buffer::tombstone_path(&root)).expect("re-enable");
|
||||
assert_eq!(
|
||||
envelope::install_age_at(&root, day("2026-11-13")),
|
||||
Some(InstallAge::NewToday)
|
||||
);
|
||||
envelope::stamp_first_seen(&root, day("2026-11-13"));
|
||||
assert_eq!(
|
||||
envelope::read_state(&root).first_seen_day.as_deref(),
|
||||
Some("2026-11-13")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_first_seen_day_never_reaches_the_wire() {
|
||||
let wire = serde_json::to_string(&every_field_batch()).expect("serialize");
|
||||
assert!(!wire.contains("first_seen"));
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"schema_version": 6,
|
||||
"notice_version": 8,
|
||||
"sent_at": "2026-08-03T18:04:11Z",
|
||||
"install_id": "3f2a9c1e-0000-4000-8000-000000000001",
|
||||
"app_version": "0.9.4",
|
||||
"git_sha": "abcdef012345",
|
||||
"surface": "control-plane",
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"libc": "none",
|
||||
"tty": true,
|
||||
"events": [
|
||||
{
|
||||
"event": "install_or_upgrade",
|
||||
"kind": "upgrade",
|
||||
"previous_version": "0.9.3-rc.1"
|
||||
},
|
||||
{
|
||||
"event": "session_start",
|
||||
"source": "fleet_worker",
|
||||
"install_age": "under_week"
|
||||
},
|
||||
{
|
||||
"event": "session_end",
|
||||
"duration_bucket": "1m_10m",
|
||||
"exit_class": "unclean",
|
||||
"cold_start_bucket": "250_1000",
|
||||
"providers": [
|
||||
"custom",
|
||||
"deepseek"
|
||||
],
|
||||
"counters": {
|
||||
"turns": 14,
|
||||
"tool_calls": 61,
|
||||
"fleet_dispatch": 0,
|
||||
"workflow_run": 0,
|
||||
"subagent_spawn": 2,
|
||||
"mcp_server_connected": 0,
|
||||
"memory_search": 0,
|
||||
"approval_modal_shown": 0,
|
||||
"approval_auto_allowed": 0,
|
||||
"command_palette_open": 3
|
||||
},
|
||||
"errors": {
|
||||
"auth_preflight_failed": 0,
|
||||
"provider_http_4xx": 0,
|
||||
"provider_http_5xx": 1,
|
||||
"tool_denied_by_policy": 0,
|
||||
"tool_timeout": 0,
|
||||
"network_error": 0
|
||||
},
|
||||
"turn_wall": {
|
||||
"lt_5s": 9,
|
||||
"5_30s": 4,
|
||||
"30_120s": 1,
|
||||
"gte_120s": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "panic",
|
||||
"site": "crates/tui/src/tui/ui.rs:8801:17"
|
||||
},
|
||||
{
|
||||
"event": "product_usage",
|
||||
"counters": {
|
||||
"page_view": 1,
|
||||
"docs_view": 0,
|
||||
"install_copy": 0,
|
||||
"download": 0,
|
||||
"signup": 0,
|
||||
"login": 0,
|
||||
"session_create": 0,
|
||||
"session_resume": 0,
|
||||
"turn_submit": 0,
|
||||
"turn_complete": 0,
|
||||
"settings_open": 0,
|
||||
"integration_connect": 0,
|
||||
"error_shown": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "operations_summary",
|
||||
"requests": 10,
|
||||
"errors": 1,
|
||||
"duration_ms_total": 1500,
|
||||
"duration_ms_max": 300,
|
||||
"probes": 2,
|
||||
"probes_failed": 0
|
||||
},
|
||||
{
|
||||
"event": "model_usage",
|
||||
"provider": "deepseek",
|
||||
"model": "other",
|
||||
"counters": {
|
||||
"turns_ok": 12,
|
||||
"turns_failed": 1,
|
||||
"turns_interrupted": 0,
|
||||
"input_tokens": 418230,
|
||||
"output_tokens": 9120,
|
||||
"cache_read_tokens": 380001,
|
||||
"cache_write_tokens": 0,
|
||||
"requests": 27,
|
||||
"reasoning_tokens": 2310,
|
||||
"generation_secs": 412
|
||||
},
|
||||
"turn_wall": {
|
||||
"lt_5s": 3,
|
||||
"5_30s": 8,
|
||||
"30_120s": 2,
|
||||
"gte_120s": 0
|
||||
},
|
||||
"context": {
|
||||
"lt_32k": 9,
|
||||
"32k_128k": 11,
|
||||
"128k_256k": 5,
|
||||
"gte_256k": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "tool_usage",
|
||||
"tool": "read_file",
|
||||
"calls": 41,
|
||||
"errors": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2515,10 +2515,7 @@ fn arm_telemetry_with_setup(
|
||||
};
|
||||
codewhale_telemetry::init(consent.with_config_path(Some(store.path().to_path_buf())));
|
||||
let _ = TELEMETRY_SESSION_START.set(std::time::Instant::now());
|
||||
codewhale_telemetry::record(codewhale_telemetry::Event::SessionStart {
|
||||
source,
|
||||
install_age: codewhale_telemetry::install_age(),
|
||||
});
|
||||
codewhale_telemetry::record(codewhale_telemetry::Event::SessionStart { source });
|
||||
}
|
||||
|
||||
fn arm_telemetry(cli: &Cli, command: Option<&Commands>) {
|
||||
|
||||
@@ -51,6 +51,13 @@ fn resolve(
|
||||
directory
|
||||
.parent()
|
||||
.ok_or("test deps directory has no parent")?
|
||||
} else if directory.ends_with("out") {
|
||||
directory
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.find(|ancestor| ancestor.ends_with("build"))
|
||||
.and_then(Path::parent)
|
||||
.unwrap_or(directory)
|
||||
} else {
|
||||
directory
|
||||
};
|
||||
@@ -200,4 +207,27 @@ mod tests {
|
||||
assert_eq!(resolve(None, None, &test, &target).unwrap(), canonical);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_unit_build_layout_resolves_the_final_target_and_cross_profile() {
|
||||
let root = tempfile::TempDir::new().unwrap();
|
||||
let target = root.path().join("final target");
|
||||
for triple in [None, Some("aarch64-unknown-linux-gnu")] {
|
||||
let final_profile = match triple {
|
||||
Some(triple) => target.join(triple).join("debug"),
|
||||
None => target.join("debug"),
|
||||
};
|
||||
std::fs::create_dir_all(&final_profile).unwrap();
|
||||
let canonical =
|
||||
final_profile.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
|
||||
std::fs::write(&canonical, b"canonical").unwrap();
|
||||
let test = final_profile
|
||||
.join("build")
|
||||
.join("codewhale-tui")
|
||||
.join("0123456789abcdef")
|
||||
.join("out")
|
||||
.join("integration-hash");
|
||||
assert_eq!(resolve(None, None, &test, &target).unwrap(), canonical);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -17,7 +17,7 @@ they do not cost context on machines that will never hit them.
|
||||
worktrees). Those spawned shells only see the repo's `rust-toolchain.toml`
|
||||
override while inside `/workspace`, so without a global default they fail with
|
||||
"rustup could not choose a version of rustc to run". The update script runs
|
||||
`rustup default stable` to fix this.
|
||||
`rustup default 1.100.0` to fix this.
|
||||
- **Known env-specific test failures at `/workspace` (not code bugs):** because
|
||||
the checkout sits directly under `/`, two `codewhale-tui` subagent tests fail
|
||||
here — `git_repo_root_reports_attempted_paths_when_no_repo_found` (cannot
|
||||
@@ -172,6 +172,15 @@ exports; the x64 profile now allows 471 pairs and the ARM64 profile 473. The
|
||||
same limit applies: the candidate still has to pass the import check against
|
||||
its final executable bytes, and neither addition is a 14393 or installer run.
|
||||
|
||||
Release candidate run 43320217667, built after the stable toolchain moved to
|
||||
Rust 1.100.0, stopped the check on two more kernel32.dll pairs,
|
||||
`GetLogicalDrives` and `QueryDosDeviceW`, which the Rust 1.100.0 standard
|
||||
library's Windows file system code imports. Probe run 43330905781 (Windows 11
|
||||
ARM64 build 26200.9725) resolved all 449 imports of that candidate's ARM64
|
||||
executables. For x64 the 2 pairs are documented Microsoft API contracts
|
||||
(Windows XP minimum on each function's reference page); the x64 profile now
|
||||
allows 473 pairs and the ARM64 profile 475, with the same limits as above.
|
||||
|
||||
At this snapshot, startup/installation of the final release bytes on an actual
|
||||
1607 machine and the candidate's generated uninstaller were still unqualified.
|
||||
The final frozen source, release import dumps, archive/native/npm installation,
|
||||
|
||||
+11
-11
@@ -49,8 +49,8 @@ cargo build --release --locked -p codewhale-cli
|
||||
target/release/codewhale --version
|
||||
```
|
||||
|
||||
See the Cargo section for build prerequisites; use the repository's stable Rust
|
||||
selection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`
|
||||
See the Cargo section for build prerequisites; use the repository's pinned Rust
|
||||
1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`
|
||||
and needs MSVC. A development build is separate from a signed release package.
|
||||
For mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).
|
||||
The [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is
|
||||
@@ -543,25 +543,25 @@ cargo install --path crates/cli --locked # installs ~/.cargo/bin/codewhale
|
||||
|
||||
Things to know (observed):
|
||||
|
||||
* The repo contains `rust-toolchain.toml` (`channel = "stable"`). The first
|
||||
`cargo` command inside the checkout **silently downloads the latest stable
|
||||
toolchain** (about 250 MB), whatever your default is.
|
||||
* The workspace treats every compiler warning as an error. With Rust 1.89
|
||||
the build **fails** after about 11 minutes with 8
|
||||
`error: this lint expectation is unfulfilled` errors in `codewhale-tui`. Use
|
||||
the stable toolchain the repo selects. Don't pass an older `+toolchain`.
|
||||
* The repo contains `rust-toolchain.toml` (`channel = "1.100.0"`). With rustup,
|
||||
the first `cargo` command inside the checkout **silently downloads Rust
|
||||
1.100.0** (about 250 MB) if you do not have it, whatever your default is.
|
||||
* The workspace treats every compiler warning as an error. The declared
|
||||
minimum is Rust 1.89, which CI's MSRV job checks, but release binaries and
|
||||
the repository's own checks use 1.100.0. Without rustup, use rustc 1.89 or
|
||||
newer.
|
||||
* The workspace release profile uses thin LTO. On a 4-vCPU / 15 GB VM, the
|
||||
`codewhale-tui` crate alone compiled for more than an hour, peaking at
|
||||
6–8 GB of RAM. Budget 16 GB or more, and expect this to be the slowest install
|
||||
path by far. `target/` grew past 1.2 GB.
|
||||
|
||||
Tested result: **works** on the repo-selected stable Rust (1.98.1).
|
||||
Tested result: **works** on Rust 1.98.1, the version the repository selected when this was measured.
|
||||
* `Finished release profile … in 83m 12s`. A Nix build competed for CPU and
|
||||
memory for most of that time, so treat it as an upper bound.
|
||||
* `target/` ended at **3.1 GB**. Delete it afterwards with `cargo clean`.
|
||||
* The binary reports `codewhale 0.10.0 (dev)`, and `exec` worked.
|
||||
* Cargo prints `warning: default toolchain implicitly overridden with
|
||||
stable-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.
|
||||
1.100.0-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.
|
||||
* **Cargo builds provide no `codew`.**
|
||||
|
||||
**Upgrade:** re-run the same `cargo install … --force` (for crates.io, you can
|
||||
|
||||
+19
-59
@@ -4,7 +4,7 @@
|
||||
|
||||
**Usage analytics are on by default in the current source.** The first
|
||||
interactive launch gives a localized, nonblocking disclosure naming **Codewhale
|
||||
and PostHog**, with a direct route to turn counting off. Notice version `8`
|
||||
and PostHog**, with a direct route to turn counting off. Notice version `6`
|
||||
describes this policy; displaying it is not recorded as human acceptance.
|
||||
Missing an older acceptance record does not turn a fresh installation off.
|
||||
Existing explicit opt-outs remain off across upgrades, and unreadable privacy
|
||||
@@ -65,7 +65,7 @@ codewhale --telemetry false # the same kill switch, for one command
|
||||
**`telemetry = false` in the config file is the opt-out.** It is a floor:
|
||||
`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a
|
||||
setting you can undo by accident from a wrapper script is not a setting. It
|
||||
deletes the random install id and the local first-seen day (so the install-age bucket starts over at `new_today` if you turn telemetry back on), truncates every buffered event and every dry-run
|
||||
deletes the random install id, truncates every buffered event and every dry-run
|
||||
record, and writes a tombstone. Appends, identity/state writes, and delivery all
|
||||
share the wipe's ordering lock, so once opt-out returns no pre-opt-out write or
|
||||
POST remains in flight. If any part of that wipe fails, the tombstone is still
|
||||
@@ -104,7 +104,7 @@ Everything is under `$CODEWHALE_HOME/telemetry/` (`0700`), every file `0600`:
|
||||
| `buffer.jsonl` | pending events, one JSON object per line |
|
||||
| `buffer.jsonl.lock` | the sibling ordering lock shared by writes, delivery, arming, and wipe |
|
||||
| `dryrun.jsonl` | where batches go when the endpoint is configured empty |
|
||||
| `state.json` | the last app version seen, the last flush attempt, and the UTC day this state was first created. The date never leaves the device: only the `install_age` bucket is sent |
|
||||
| `state.json` | the last app version seen and the last flush attempt |
|
||||
| `install_id.json` | the random install id and when it was minted |
|
||||
| `disabled` | the tombstone; present means nothing is appended or sent |
|
||||
|
||||
@@ -162,14 +162,10 @@ most once per flush point and never grows a queue.
|
||||
|
||||
## Event schema
|
||||
|
||||
`SCHEMA_VERSION = 6`. `notice_version = 8` identifies the disclosed default-on
|
||||
policy; it does not assert human consent or that a notice was seen. Schema 6
|
||||
adds, to `model_usage`, the request count, reasoning tokens, whole seconds spent
|
||||
waiting on the model and a four-bucket input-size histogram; adds a coarse
|
||||
install age and three exec start labels to `session_start`; and adds the
|
||||
`unclean` exit class. Schema 4 added the `model_usage` and `tool_usage` events
|
||||
and the `ci` session source. Schema-4 and schema-3 batches are still accepted
|
||||
without the schema-6 fields. Schema-3 batches with `notice_version = 5` are still accepted without them, which
|
||||
`SCHEMA_VERSION = 4`. `notice_version = 6` identifies the disclosed default-on
|
||||
policy; it does not assert human consent or that a notice was seen. Schema 4
|
||||
adds the `model_usage` and `tool_usage` events and the `ci` session source.
|
||||
Schema-3 batches with `notice_version = 5` are still accepted without them, which
|
||||
is what the web and desktop control-plane relay sends. The closed
|
||||
v2 contract still accepts only `consent_version = 4`, retaining its original
|
||||
explicit opt-in meaning. Fields cannot be mixed between versions. The unchanged
|
||||
@@ -182,8 +178,8 @@ new fields, surfaces, and `product_usage` events.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema_version": 6,
|
||||
"notice_version": 8,
|
||||
"schema_version": 4,
|
||||
"notice_version": 6,
|
||||
"sent_at": "2026-08-03T18:04:11Z", // RFC3339 UTC, second precision
|
||||
"install_id": "3f2a…", // uuid v4, rotates every 90 days
|
||||
"app_version": "0.9.12",
|
||||
@@ -200,7 +196,7 @@ new fields, surfaces, and `product_usage` events.
|
||||
| Field | Type | Source anchor | Rule |
|
||||
|---|---|---|---|
|
||||
| `schema_version` | `u32` | const in `crates/telemetry/src/event.rs` | Bumped on any field add/remove/retype. Never reused. Pinned by a golden snapshot test. |
|
||||
| `notice_version` | `u32` | policy constant in `crates/telemetry/src/event.rs` | Exactly `8` for schema 6; identifies the disclosed opt-out policy, not a human acceptance record. |
|
||||
| `notice_version` | `u32` | policy constant in `crates/telemetry/src/event.rs` | Exactly `6` for schema 4; identifies the disclosed opt-out policy, not a human acceptance record. |
|
||||
| `sent_at` | RFC3339 | `chrono::Utc::now()` | Second precision. Per-**batch** only — events carry no timestamps at all. |
|
||||
| `install_id` | uuid v4 | `crates/telemetry/src/envelope.rs` | Random, never derived, rotated every 90 days. See "Where it lives" above. |
|
||||
| `app_version` | string | `env!("CARGO_PKG_VERSION")`, as at `crates/telemetry/src/lib.rs:112` | Must match `^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$`. |
|
||||
@@ -288,27 +284,14 @@ Emitted once when `state.json`'s `last_version` differs from `app_version`.
|
||||
### Event: `session_start`
|
||||
|
||||
```jsonc
|
||||
{ "event": "session_start", "source": "interactive", "install_age": "older" }
|
||||
{ "event": "session_start", "source": "interactive" }
|
||||
```
|
||||
|
||||
`source` is `SessionSource` (`crates/telemetry/src/event.rs`), a closed enum declared in the telemetry crate: `interactive | resume | fork | api | unknown | ci | fleet_worker | agent | scripted`.
|
||||
`source` is `SessionSource` (`crates/state/src/lib.rs:34-41`) stringified by `session_source_to_str` (`:1909-1917`): `interactive | resume | fork | api | unknown | ci`.
|
||||
`ci` replaces the others whenever `CI` or `GITHUB_ACTIONS` is set to a true
|
||||
value, including the Codewhale GitHub Action, so automated runs are never
|
||||
counted as people. Only that boolean environment fact is read.
|
||||
|
||||
`fleet_worker`, `agent` and `scripted` separate automated `exec` sessions from
|
||||
people. `fleet_worker` is an `exec` that Codewhale's own fleet dispatcher
|
||||
started, `agent` is an `exec` started from inside an agent's shell command (only
|
||||
the presence of a Codewhale-owned environment variable is read, never its
|
||||
value), and `scripted` is an `exec` whose standard input or output is not a
|
||||
terminal. When more than one applies the order is `fleet_worker`, `agent`, `ci`,
|
||||
`scripted`. Other surfaces are unchanged.
|
||||
|
||||
`install_age` is `new_today | under_week | older | null`: a coarse bucket of how
|
||||
long this installation has existed. It is computed on your device and only the
|
||||
bucket leaves it; the date it comes from is stored only in the local state file
|
||||
and never leaves the device. The same UTC day is `new_today`, 1 to 6 days is `under_week`, and 7 or more is `older`. An installation whose state predates this field reports `older`, never `new_today`. `null` means the local state could not be read.
|
||||
|
||||
### Event: `session_end`
|
||||
|
||||
The workhorse. Everything a session accumulated ships here, once.
|
||||
@@ -334,11 +317,11 @@ The workhorse. Everything a session accumulated ships here, once.
|
||||
|
||||
**`duration_bucket`** — `chrono` delta from `app.session_started_at` (`crates/tui/src/tui/app.rs:1889`). Half-open, seconds: `lt_1m` (`d < 60`), `1m_10m` (`60 ≤ d < 600`), `10m_60m` (`600 ≤ d < 3600`), `gt_60m` (`d ≥ 3600`).
|
||||
|
||||
**`exit_class`** — `clean | signal | panic | error | unclean`. `unclean` means the process ended without its own shutdown report and its totals were recovered from a local checkpoint at a later start. **Derived from an explicit `AtomicU8`, never from an exit code.** `RunTerminationReason::Canceled` maps to exit 130 (`crates/engine/src/core/runtime_contract/termination.rs:53`), the same value the signal task uses (`crates/tui/src/lib.rs:682`, 128+SIGINT), so a code-based derivation would report every Esc-cancelled turn as a signal. The atomic is set by the panic hook (`crates/tui/src/lib.rs:1582`), by the signal task (`:678-696`) before `std::process::exit`, and on the clean path from `RunTerminationReason::is_success()` (`crates/engine/src/core/runtime_contract/termination.rs:44-46`) — `error` otherwise. Exit codes remain insufficient: an approval-required exit (3) and a terminal exec failure (1) need their actual termination classification.
|
||||
**`exit_class`** — `clean | signal | panic | error`. **Derived from an explicit `AtomicU8`, never from an exit code.** `RunTerminationReason::Canceled` maps to exit 130 (`crates/engine/src/core/runtime_contract/termination.rs:53`), the same value the signal task uses (`crates/tui/src/lib.rs:682`, 128+SIGINT), so a code-based derivation would report every Esc-cancelled turn as a signal. The atomic is set by the panic hook (`crates/tui/src/lib.rs:1582`), by the signal task (`:678-696`) before `std::process::exit`, and on the clean path from `RunTerminationReason::is_success()` (`crates/engine/src/core/runtime_contract/termination.rs:44-46`) — `error` otherwise. Exit codes remain insufficient: an approval-required exit (3) and a terminal exec failure (1) need their actual termination classification.
|
||||
|
||||
**`cold_start_bucket`** — from `startup_trace::elapsed_ms()`, which reads `PROCESS_START` directly and is independent of the startup summary's buffer clear (`crates/runtime/src/startup_trace.rs:39-45`). Boundaries: `lt_250`, `250_1000`, `1000_3000`, `gte_3000`. Absent on non-TUI surfaces.
|
||||
|
||||
**`providers`** — sorted, deduplicated array of `ProviderKind::as_str()` (`crates/config/src/provider_kind.rs:295`, a `&'static str` from a closed enum; `Custom` yields the literal `"custom"`). **The API takes `ProviderKind` by value, never `&str`.** Do not call `ProviderKind::parse` or `parse_config_identity` (`:300`, `:330`) — those are for config-table resolution. **Do not read** `provider_identity_for_persistence()` (`crates/tui/src/tui/app.rs:5049`), `provider_id_for_persistence()` (`:5058`), `ExecStreamMeta.provider_id` (`crates/app-server/src/exec_agent.rs`), or `PlannedTurnRoute.effective_provider_label` (`crates/engine/src/turn_route_plan.rs:189-193`) — all four return the customer's own `[providers.<name>]` table key when the route is Custom. This is the single most likely leak in the feature: it is one field away from the natural seam and `/status` already prints it (`crates/tui/src/commands/groups/config/status.rs:24-28`). **No model id outside the bundled public catalog is ever sent, for any provider** — `crates/runtime/src/safe_label.rs:11-15` documents that a model id can be a path, a URL, or a deployment id that is itself a credential. Those are sent as `other`; see `model_usage`.
|
||||
**`providers`** — sorted, deduplicated array of `ProviderKind::as_str()` (`crates/config/src/provider_kind.rs:295`, a `&'static str` from a closed enum; `Custom` yields the literal `"custom"`). **The API takes `ProviderKind` by value, never `&str`.** Do not call `ProviderKind::parse` or `parse_config_identity` (`:300`, `:330`) — those are for config-table resolution. **Do not read** `provider_identity_for_persistence()` (`crates/tui/src/tui/app.rs:5049`), `provider_id_for_persistence()` (`:5058`), `ExecStreamMeta.provider_id` (`crates/app-server/src/exec_agent.rs`), or `PlannedTurnRoute.effective_provider_label` (`crates/engine/src/turn_route_plan.rs:189-193`) — all four return the customer's own `[providers.<name>]` table key when the route is Custom. This is the single most likely leak in the feature: it is one field away from the natural seam and `/status` already prints it (`crates/tui/src/commands/groups/config/status.rs:24-28`). **No model id is ever sent, for any provider** — `crates/runtime/src/safe_label.rs:11-15` documents that a model id can be a path, a URL, or a deployment id that is itself a credential.
|
||||
|
||||
**`counters`** — closed field set. Every bump happens at the **call site**, never inside a conditionally-entered handler:
|
||||
|
||||
@@ -460,13 +443,9 @@ to make defaults, and whether prompt caching is working.
|
||||
"input_tokens": 418230,
|
||||
"output_tokens": 9120,
|
||||
"cache_read_tokens": 380001,
|
||||
"cache_write_tokens": 0,
|
||||
"requests": 27,
|
||||
"reasoning_tokens": 2310,
|
||||
"generation_secs": 412
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"turn_wall": { "lt_5s": 3, "5_30s": 8, "30_120s": 2, "gte_120s": 0 },
|
||||
"context": { "lt_32k": 9, "32k_128k": 11, "128k_256k": 5, "gte_256k": 2 }
|
||||
"turn_wall": { "lt_5s": 3, "5_30s": 8, "30_120s": 2, "gte_120s": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -479,25 +458,6 @@ cache reads. Turn counts are by outcome, and `turn_wall` buckets each turn's
|
||||
wall time as `session_end` does. A session reports at most 16 provider and model
|
||||
pairs; later pairs are counted under `other`. No per-turn timeline is sent.
|
||||
|
||||
Schema 6 adds four aggregates to each row:
|
||||
|
||||
- `requests` counts the model requests this session made on its own route for
|
||||
that provider and model.
|
||||
- `reasoning_tokens` is the part of `output_tokens` the provider reports as
|
||||
reasoning. `output_tokens` is not reduced, and the value is `0` when the
|
||||
provider does not report it.
|
||||
- `generation_secs` is the total whole seconds the session spent waiting on the
|
||||
model for those requests. It is one sum for the session; no per-request
|
||||
duration is ever sent.
|
||||
- `context` counts requests by input size, in tokens including cache reads:
|
||||
`lt_32k` is under 32768, `32k_128k` is 32768 up to 131072, `128k_256k` is
|
||||
131072 up to 262144, and `gte_256k` is 262144 or more. The four buckets add up
|
||||
to `requests`. Each edge belongs to the higher bucket. No per-request size is
|
||||
sent.
|
||||
|
||||
Until a client fills them these values are `0`. A schema-4 row carries none of
|
||||
these fields.
|
||||
|
||||
### Event: `tool_usage`
|
||||
|
||||
One event per tool a session called, sent with `session_end`. It is how
|
||||
@@ -548,7 +508,7 @@ configured ingest may forward validated schema-v3 / notice-v5 batches and
|
||||
legacy schema-v2 / consent-v4 batches to
|
||||
PostHog's [batch capture API](https://posthog.com/docs/api/capture). The only
|
||||
permitted origins are `https://us.i.posthog.com` and `https://eu.i.posthog.com`.
|
||||
Schema-v1, schema-v4 and schema-v6 batches cannot reach that path in this source. PostHog receives the same bounded
|
||||
Schema-v1 and schema-v4 batches cannot reach that path in this source. PostHog receives the same bounded
|
||||
envelope and event fields, the batch timestamp as its event timestamp, and
|
||||
`codewhale:<install_id>` as its anonymous `distinct_id`. Events are named
|
||||
`codewhale_install_or_upgrade`, `codewhale_session_start`,
|
||||
@@ -598,7 +558,7 @@ It prints the daily series, a 7-day trend over complete UTC days, the freshness
|
||||
|
||||
### What is never collected — the public red-line list
|
||||
|
||||
Prompts; completions; tool arguments; diffs; patches; file contents; filenames; absolute or relative paths; git remotes; repo names; branch names; workspace commit SHAs; memory entries; chat history; API keys, tokens, cookies, or `Authorization` headers (including any boolean asserting a key exists); model ids that are not in Codewhale's bundled public model catalog (custom, local, fine-tuned or private model names are sent only as `other`); custom provider table names; MCP server names, commands, URLs, or tool names; plugin and configured tool names; approval rule text; error message bodies; panic message text; per-event timestamps; per-request durations or sizes; environment variable values; CI vendor or job names; parent process names; hostnames; container ids; the date an installation was first seen (only the install-age bucket leaves the device); keystrokes; clipboard; screenshots; microphone; camera; location; and any third-party ad or analytics SDK — there are none in the runtime binary and none may be added.
|
||||
Prompts; completions; tool arguments; diffs; patches; file contents; filenames; absolute or relative paths; git remotes; repo names; branch names; workspace commit SHAs; memory entries; chat history; API keys, tokens, cookies, or `Authorization` headers (including any boolean asserting a key exists); model ids that are not in Codewhale's bundled public model catalog (custom, local, fine-tuned or private model names are sent only as `other`); custom provider table names; MCP server names, commands, URLs, or tool names; plugin and configured tool names; approval rule text; error message bodies; panic message text; per-event timestamps; keystrokes; clipboard; screenshots; microphone; camera; location; and any third-party ad or analytics SDK — there are none in the runtime binary and none may be added.
|
||||
|
||||
Two named traps for the implementer. `crates/state/src/lib.rs` persists `git_sha`, `git_branch`, `git_origin_url`, `cwd`, and `path` on the threads table (`:93, :399, :653`): a payload builder that accepts a `Thread` or `ThreadMeta` and derives `Serialize` breaches the contract in one line. **Never derive `Serialize` over an existing state type** — build every telemetry struct from scratch with explicit fields. And `crates/core/src/lib.rs:1389-1398` is the one place in the tree where the word `telemetry` sits inside a JSON object next to `prompt`, `base_url`, and `has_api_key`. It is the object someone will copy. Do not.
|
||||
|
||||
@@ -607,7 +567,7 @@ Two named traps for the implementer. `crates/state/src/lib.rs` persists `git_sha
|
||||
## Website funnel aggregates in the source candidate
|
||||
|
||||
The website extension uses `FUNNEL_SCHEMA_VERSION = 5` and notice version `7`.
|
||||
The Rust runtime is on `SCHEMA_VERSION = 6`, notice `8`; existing browser
|
||||
The Rust runtime remains on `SCHEMA_VERSION = 4`, notice `6`; existing browser
|
||||
schema `3`, notice `5`, and legacy schemas `1` and `2` retain their original
|
||||
contracts. Displaying a newer notice never overrides an opt-out or establishes
|
||||
human acceptance. Schema 5 is first-party only and does not activate PostHog.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
子智能体(subagent)工作树)。这些被拉起的 shell 只有在 `/workspace` 内部
|
||||
才能看到仓库的 `rust-toolchain.toml` 覆盖设置,所以没有全局默认值时,
|
||||
它们会以“rustup could not choose a version of rustc to run”失败。
|
||||
更新脚本会运行 `rustup default stable` 来修复这一点。
|
||||
更新脚本会运行 `rustup default 1.100.0` 来修复这一点。
|
||||
- **`/workspace` 上已知的环境相关测试失败(不是代码缺陷):** 因为检出目录
|
||||
直接位于 `/` 之下,有两个 `codewhale-tui` 子智能体测试会在这里失败——
|
||||
`git_repo_root_reports_attempted_paths_when_no_repo_found`(无法在不可写的
|
||||
@@ -132,6 +132,8 @@ x64 没有导出探测,因此这 12 对作为低于 14393 下限的微软文
|
||||
x64 的这 3 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP),与 #6953 报告者实测导出分开标注;x64 允许列表现为 471 对,ARM64 为 473 对。
|
||||
限制不变:候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
|
||||
|
||||
候选运行 43320217667 在稳定工具链升级到 Rust 1.100.0 后,又在 kernel32.dll 的 `GetLogicalDrives` 与 `QueryDosDeviceW` 两对上停止;它们来自 Rust 1.100.0 标准库的 Windows 文件系统代码。探测运行 43330905781(Windows 11 ARM64 build 26200.9725)解析了该候选 ARM64 可执行文件的全部 449 个导入。x64 的这 2 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP);x64 允许列表现为 473 对,ARM64 为 475 对,限制同上。
|
||||
|
||||
此快照中,最终发布文件在真实 1607 上启动/安装及候选程序生成的卸载器仍未验收。
|
||||
最终冻结源码、发布导入记录、各安装途径、打包/原生应用崩溃恢复和最低 OS 回执应
|
||||
进入发布验收记录,见[英文发布检查单](../RELEASE_CHECKLIST.md)。
|
||||
|
||||
@@ -28,7 +28,7 @@ cargo build --release --locked -p codewhale-cli
|
||||
target/release/codewhale --version
|
||||
```
|
||||
|
||||
构建前置条件见下文 Cargo 章节;使用仓库选定的 stable Rust,最低声明版本
|
||||
构建前置条件见下文 Cargo 章节;使用仓库固定的 Rust 1.100.0,最低声明版本
|
||||
为 1.89。首次构建可能耗时较长。Windows 的可执行文件为
|
||||
`target/release/codewhale.exe`,并需要 MSVC 工具链。开发构建不等于已签名发布包。
|
||||
|
||||
@@ -404,15 +404,15 @@ cargo install --path crates/cli --locked # installs ~/.cargo/bin/codewhale
|
||||
|
||||
需要知道的事(实际观察到的):
|
||||
|
||||
* 仓库里有 `rust-toolchain.toml`(`channel = "stable"`)。在检出目录里运行的第一条 `cargo` 命令,无论你的默认工具链是什么,都会**悄悄下载最新的 stable 工具链**(约 250 MB)。
|
||||
* workspace 把所有编译器警告都当作错误。用 Rust 1.89 时,构建会在大约 11 分钟后**失败**,在 `codewhale-tui` 中出现 8 个 `error: this lint expectation is unfulfilled`。请使用仓库选定的 stable 工具链,不要传入更旧的 `+toolchain`。
|
||||
* 仓库里有 `rust-toolchain.toml`(`channel = "1.100.0"`)。使用 rustup 时,如果本机没有 Rust 1.100.0,在检出目录里运行的第一条 `cargo` 命令,无论你的默认工具链是什么,都会**悄悄下载 Rust 1.100.0**(约 250 MB)。
|
||||
* workspace 把所有编译器警告都当作错误。声明的最低版本是 Rust 1.89,CI 的 MSRV 任务会检查它;但发布二进制和仓库自身的检查使用 1.100.0。不使用 rustup 时,请使用 1.89 或更新的 rustc。
|
||||
* workspace 的 release profile 使用 thin LTO。在 4 vCPU / 15 GB 的虚拟机上,仅 `codewhale-tui` 这一个 crate 就编译了一个多小时,内存峰值 6–8 GB。请准备 16 GB 或更多内存,并预期这是迄今最慢的安装路径。`target/` 增长超过了 1.2 GB。
|
||||
|
||||
测试结果:用仓库选定的 stable Rust(1.98.1)**可行**。
|
||||
测试结果:用 Rust 1.98.1(测量时仓库选定的版本)**可行**。
|
||||
* `Finished release profile … in 83m 12s`。其间大部分时间有一个 Nix 构建在争抢 CPU 和内存,所以请把它当作上限。
|
||||
* `target/` 最终达到 **3.1 GB**。之后用 `cargo clean` 删除。
|
||||
* 该二进制报告 `codewhale 0.10.0 (dev)`,`exec` 可用。
|
||||
* Cargo 会打印 `warning: default toolchain implicitly overridden with stable-x86_64-unknown-linux-gnu by rustup toolchain file`,这是无害的。
|
||||
* Cargo 会打印 `warning: default toolchain implicitly overridden with 1.100.0-x86_64-unknown-linux-gnu by rustup toolchain file`,这是无害的。
|
||||
* **Cargo 构建不提供 `codew`。**
|
||||
|
||||
**升级:** 重新运行同样的 `cargo install … --force`(对 crates.io 可以加 `--version X.Y.Z`)。`codewhale update` 会拒绝处理 Cargo 安装。**卸载:** `cargo uninstall codewhale-cli`,然后见 §12。
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.100.0"
|
||||
@@ -6,7 +6,7 @@
|
||||
"osArchitecture": "x64",
|
||||
"qualification": {
|
||||
"result": "PASS",
|
||||
"kind": "reporter exact-OS export resolution plus 15 documented Microsoft named API minima",
|
||||
"kind": "reporter exact-OS export resolution plus 17 documented Microsoft named API minima",
|
||||
"evidence": {
|
||||
"issue": "https://github.com/codewhale-hq/Codewhale/issues/6953",
|
||||
"binary": "https://github.com/codewhale-hq/Codewhale/releases/download/v0.10.1/codewhale-windows-x64.exe",
|
||||
@@ -16,7 +16,7 @@
|
||||
"previous_sha256": "f7b35a1e928f67884637f36b01fc72854f50690130f191bac3bcf62ece3c1d84"
|
||||
},
|
||||
"runtimeAcceptance": "Final 0.10.2 release bytes on 14393 not run",
|
||||
"qualifiedPairs": 471,
|
||||
"qualifiedPairs": 473,
|
||||
"namedApiContracts": [
|
||||
{
|
||||
"dll": "advapi32.dll",
|
||||
@@ -107,6 +107,18 @@
|
||||
"symbol": "GetSidSubAuthorityCount",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthoritycount"
|
||||
},
|
||||
{
|
||||
"dll": "kernel32.dll",
|
||||
"symbol": "GetLogicalDrives",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives"
|
||||
},
|
||||
{
|
||||
"dll": "kernel32.dll",
|
||||
"symbol": "QueryDosDeviceW",
|
||||
"minimumClient": "Windows XP",
|
||||
"source": "https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-querydosdevicew"
|
||||
}
|
||||
],
|
||||
"namedApiContractEvidence": {
|
||||
@@ -128,6 +140,17 @@
|
||||
"advapi32.dll!GetSidSubAuthorityCount"
|
||||
],
|
||||
"scope": "Three pairs from crates/config private_directory.rs; documentation-backed static contracts, same limits as above"
|
||||
},
|
||||
{
|
||||
"candidateRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43320217667",
|
||||
"source": "b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"binarySha256": "b9abea95d29ed0c6caae1373675f7e45e6fe6099b15dae9a99ac64f170263681",
|
||||
"importReceiptSha256": "9bfddf3ed0633d23184b402cb02a880306847a6de43116bd1fabd19bea764544",
|
||||
"pairs": [
|
||||
"kernel32.dll!GetLogicalDrives",
|
||||
"kernel32.dll!QueryDosDeviceW"
|
||||
],
|
||||
"scope": "Two pairs that the Rust 1.100.0 standard library's Windows file system code imports (the toolchain, not a Codewhale call site); documentation-backed static contracts, same limits as above"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -421,6 +444,7 @@
|
||||
"GetFullPathNameA",
|
||||
"GetFullPathNameW",
|
||||
"GetLastError",
|
||||
"GetLogicalDrives",
|
||||
"GetModuleFileNameW",
|
||||
"GetModuleHandleA",
|
||||
"GetModuleHandleExA",
|
||||
@@ -487,6 +511,7 @@
|
||||
"PostQueuedCompletionStatus",
|
||||
"Process32FirstW",
|
||||
"Process32NextW",
|
||||
"QueryDosDeviceW",
|
||||
"QueryInformationJobObject",
|
||||
"QueryPerformanceCounter",
|
||||
"QueryPerformanceFrequency",
|
||||
@@ -666,25 +691,25 @@
|
||||
"qualification": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-11-12T10:08:11.6545236Z",
|
||||
"time": "2026-11-12T23:41:18.0140691Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
|
||||
"source": "b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/b97e38645f13f9f5828fd8a013f414955f46d528",
|
||||
"binarySha256": "4d7dfba5955681c0e0d7cb6076a7e1e2ecf253711dcfb2931618ee97ccfdb470",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 447,
|
||||
"resolved": 449,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43320217667",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43330905781",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43320217667",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
@@ -718,25 +743,25 @@
|
||||
"supersedes": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-11-07T16:01:13.7658879Z",
|
||||
"time": "2026-11-12T10:08:11.6545236Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
|
||||
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
|
||||
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 444,
|
||||
"resolved": 447,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
@@ -770,66 +795,119 @@
|
||||
"supersedes": {
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-10-16T10:57:53.0754367Z",
|
||||
"time": "2026-11-07T16:01:13.7658879Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9457",
|
||||
"osBuild": "26200.9725",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
|
||||
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
|
||||
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 445,
|
||||
"resolved": 444,
|
||||
"unresolved": 0,
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
|
||||
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
"systemModuleHashes": {
|
||||
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
|
||||
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
|
||||
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
|
||||
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
|
||||
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
|
||||
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
|
||||
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
|
||||
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
|
||||
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
|
||||
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
|
||||
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
|
||||
"advapi32.dll": "31a87f532c0e99521de26fb25d1ec33cfbb92359952580c88350891602b6cf35",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "06bf19bcd0b4af9e78c46fc81fe8fb1bf950fb5f295d44de889c5028129b5b56",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
|
||||
"bcrypt.dll": "2a016e6f29683a7e6be564424d286d0885ff4846fff87cefab4d4e5df47a8d4a",
|
||||
"bcryptprimitives.dll": "6626bf08ddb38e3c37804d7a000c9684977fb0f5f7ab8eb1c0a91a654ec975fc",
|
||||
"combase.dll": "6082ef654c1a0978e127a1e045cb3078b1e80f8f7108b5c6cb4bc601c69b7f51",
|
||||
"crypt32.dll": "3f53b3351e75cb8847b87ce40349d48279cdddab4b19c4c6c9ad38eef6aa6baf",
|
||||
"kernel32.dll": "905c5e00f64ddf9e3439a617b25d4dd1bd0fe54143ade689422b89ccb324ba3a",
|
||||
"ntdll.dll": "0a7201420df35a67e76aa2f2f4adb793b0c0b4fbc927d7da112268a7beb74930",
|
||||
"oleaut32.dll": "61cd70f21624866fb46102644d5af24563a30c85921ed8deed89139cab340f0b",
|
||||
"shell32.dll": "b9a118ff822ab1e950c7a1009de223ecfea5d5f0a23b8a969d58ff19ce28d9cf",
|
||||
"shlwapi.dll": "b4df8c76ea7999912ddf12ba026c5f4a55b683fe9234878f506080575481779a",
|
||||
"user32.dll": "b95f8cce02f51a7cbf77f17582ad3eee8abc577c324963d0026e8dbb977fa097",
|
||||
"userenv.dll": "e9b94fa6123048d535265214322a6a09e450dd85941292bf5258bf39d5b86f9e",
|
||||
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
|
||||
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
|
||||
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
|
||||
"winmm.dll": "4fd628e0a495b1fa7e7653b562a4f5484592c9cfe733184f9808dfce18e26680",
|
||||
"ws2_32.dll": "d10c37755ef07b55829406cfaa22c623ae6f86cce5a5fed42d77d75c29ae824a"
|
||||
},
|
||||
"supersedes": {
|
||||
"time": "2026-10-13T10:43:07.5742747Z",
|
||||
"osVersion": "10.0.26300",
|
||||
"osBuild": "26300",
|
||||
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
|
||||
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
|
||||
"resolved": 453,
|
||||
"result": "PASS",
|
||||
"kind": "exact native system export resolution",
|
||||
"time": "2026-10-16T10:57:53.0754367Z",
|
||||
"osVersion": "10.0.26200.0",
|
||||
"osBuild": "26200.9457",
|
||||
"processArchitecture": "ARM64",
|
||||
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
|
||||
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
|
||||
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
|
||||
"binaries": [
|
||||
"codewhale-windows-arm64.exe",
|
||||
"codew-windows-arm64.exe",
|
||||
"codewhale-tui-windows-arm64.exe"
|
||||
],
|
||||
"resolved": 445,
|
||||
"unresolved": 0,
|
||||
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
|
||||
"profile": "debug, default features",
|
||||
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
|
||||
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
|
||||
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
|
||||
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
|
||||
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
|
||||
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
|
||||
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
|
||||
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
|
||||
"systemModuleHashes": {
|
||||
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
|
||||
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
|
||||
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
|
||||
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
|
||||
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
|
||||
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
|
||||
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
|
||||
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
|
||||
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
|
||||
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
|
||||
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
|
||||
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
|
||||
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
|
||||
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
|
||||
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
|
||||
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
|
||||
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
|
||||
},
|
||||
"supersedes": {
|
||||
"time": "2026-10-13T10:43:07.5742747Z",
|
||||
"osVersion": "10.0.26300",
|
||||
"osBuild": "26300",
|
||||
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
|
||||
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
|
||||
"resolved": 453,
|
||||
"unresolved": 0,
|
||||
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
|
||||
"profile": "debug, default features",
|
||||
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1200,7 @@
|
||||
"GetFullPathNameA",
|
||||
"GetFullPathNameW",
|
||||
"GetLastError",
|
||||
"GetLogicalDrives",
|
||||
"GetModuleFileNameW",
|
||||
"GetModuleHandleA",
|
||||
"GetModuleHandleExA",
|
||||
@@ -1190,6 +1269,7 @@
|
||||
"PostQueuedCompletionStatus",
|
||||
"Process32FirstW",
|
||||
"Process32NextW",
|
||||
"QueryDosDeviceW",
|
||||
"QueryInformationJobObject",
|
||||
"QueryPerformanceCounter",
|
||||
"QueryPerformanceFrequency",
|
||||
|
||||
@@ -15,9 +15,9 @@ hostname is the only way in.
|
||||
|
||||
The current Engine source defaults usage analytics on under notice version 6, with a
|
||||
clear opt-out. Prior explicit declines remain off; a missing acceptance record
|
||||
is not treated as consent. Schema 6 carries `notice_version: 8` and schema 4 carries `notice_version: 6`; schema 3
|
||||
is not treated as consent. Schema 4 carries `notice_version: 6`; schema 3
|
||||
uses notice 5, and legacy schema 2 retains its original explicit
|
||||
`consent_version: 4` contract. Schema 1, 4 and 6 batches are first-party
|
||||
`consent_version: 4` contract. Schema 1 and schema 4 batches are first-party
|
||||
only in this source. Local validation is not proof this updated Worker has been deployed. A user who wants
|
||||
to contact nobody sets `telemetry_endpoint = ""`, which writes batches to
|
||||
`$CODEWHALE_HOME/telemetry/dryrun.jsonl` instead.
|
||||
@@ -38,7 +38,7 @@ require separate operator approval; none is established by the local tests.
|
||||
|
||||
Validated schema 3 batches carrying `notice_version: 5` and legacy schema 2
|
||||
batches carrying explicit `consent_version: 4` can reach the processor. Schema 1
|
||||
and schema 4 and 6 batches remain first-party only. Capture uses
|
||||
and schema 4 batches remain first-party only. Capture uses
|
||||
`/batch/`, fixed anonymous/no-geo controls, no incoming headers, no cookies,
|
||||
no redirects, no response parsing, no retries, and a 1.5-second timeout.
|
||||
PostHog failure is isolated from the successful Analytics Engine write.
|
||||
@@ -143,21 +143,20 @@ take the next free slot.
|
||||
| `blob8` | `tty` — `'true'` \| `'false'` |
|
||||
| `blob9` | `install_kind` (`install_or_upgrade` only) |
|
||||
| `blob10` | `previous_version` (`install_or_upgrade` only) |
|
||||
| `blob11` | `session_source` (`session_start` only; schema 6 adds `fleet_worker`, `agent`, `scripted`) |
|
||||
| `blob12` | `duration_bucket` (`session_end`); `install_age` (`session_start`, schema 6 only: `new_today`, `under_week`, `older`, `''` for null or older schemas) |
|
||||
| `blob13` | `exit_class` (`session_end` only; schema 6 adds `unclean`) |
|
||||
| `blob11` | `session_source` (`session_start` only) |
|
||||
| `blob12` | `duration_bucket` (`session_end` only) |
|
||||
| `blob13` | `exit_class` (`session_end` only) |
|
||||
| `blob14` | `cold_start_bucket` (`session_end` only; `''` on surfaces that do not measure it) |
|
||||
| `blob15` | `providers`, comma-joined, already sorted and deduplicated; the one `provider` on `model_usage` |
|
||||
| `blob16` | `panic_site` (`panic`) — a `crates/…` path or the literal `<dep>`; `model` (`model_usage`); `tool` (`tool_usage`) |
|
||||
| `blob17` | `sent_at` — the *batch* timestamp. Events carry none. |
|
||||
| `blob18` | `aggregate_counters` — closed JSON counts, for `product_usage` or `operations_summary` |
|
||||
| `blob19` | `schema_version` |
|
||||
| `blob20` | `privacy_version`: notice8 for v6, notice7 for the v5 website funnel, notice6 for v4, notice5 for v3, consent4 for v2, empty for v1; `blob19` keeps their meanings distinct |
|
||||
| `blob20` | `privacy_version`: notice6 for v4, notice5 for v3, consent4 for v2, empty for v1; `blob19` keeps their meanings distinct |
|
||||
| `double1..10` | `counters`: `turns`, `tool_calls`, `fleet_dispatch`, `workflow_run`, `subagent_spawn`, `mcp_server_connected`, `memory_search`, `approval_modal_shown`, `approval_auto_allowed`, `command_palette_open` |
|
||||
| `double11..16` | `errors`: `auth_preflight_failed`, `provider_http_4xx`, `provider_http_5xx`, `tool_denied_by_policy`, `tool_timeout`, `network_error` |
|
||||
| `double17..20` | `turn_wall`: `lt_5s`, `5_30s`, `30_120s`, `gte_120s` |
|
||||
| `double1..11` on `model_usage` | `turns_ok`, `turns_failed`, `turns_interrupted`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, then `turn_wall` `lt_5s`, `5_30s`, `30_120s`, `gte_120s` |
|
||||
| `double12..18` on `model_usage` | schema 6 only, `0` on v4 rows (`blob19` tells them apart): `requests`, `reasoning_tokens`, `generation_secs` (whole seconds, session sum, u32), then input-size request counts `lt_32k`, `32k_128k`, `128k_256k`, `gte_256k` |
|
||||
| `double1..2` on `tool_usage` | `calls`, `errors` |
|
||||
|
||||
Columns not relevant to an event are `''` / `0`. `tty` is a blob because the 20
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CAVEATS = [
|
||||
"Runtime install means first initialized enabled telemetry state, not all installations or new people. IDs rotate every 90 days and are removed on opt-out.",
|
||||
"Native success is observed model turns_ok, not a first-ever primary or useful task. Missing usage, same-process child activity and late cancellation can affect it.",
|
||||
"Dates are received batch dates rather than exact event times. Offline, opted-out and dropped deliveries are absent; ingest has no replay deduplication guarantee.",
|
||||
"Native IDs with an observed CI, fleet worker, agent or scripted start are excluded in full because the aggregate schema cannot assign model counts to individual sessions.",
|
||||
"Native IDs with an observed CI start are excluded in full because the aggregate schema cannot assign model counts to individual sessions.",
|
||||
];
|
||||
|
||||
function instant(value, field) {
|
||||
@@ -72,7 +72,7 @@ export function buildFunnelReport(input, { now = new Date() } = {}) {
|
||||
const installs = new Map(); const successes = new Map(); const started = new Set(); const ci = new Set();
|
||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
for (const row of input.native_rows) {
|
||||
if (!uuid.test(row.install_id) || !NATIVE_SURFACES.includes(row.surface) || ![1, 2, 3, 4, 6].includes(count(row.schema_version, "schema_version"))) throw new Error("invalid native identity, surface or version");
|
||||
if (!uuid.test(row.install_id) || !NATIVE_SURFACES.includes(row.surface) || ![1, 2, 3, 4].includes(count(row.schema_version, "schema_version"))) throw new Error("invalid native identity, surface or version");
|
||||
const at = instant(fresh(row.received_at), "received_at");
|
||||
if (!within(at, from, to)) throw new Error("native row outside coverage");
|
||||
if (newest === null || at > newest) newest = at;
|
||||
@@ -82,11 +82,11 @@ export function buildFunnelReport(input, { now = new Date() } = {}) {
|
||||
if (!["install", "upgrade", "downgrade"].includes(row.install_kind)) throw new Error("invalid install kind");
|
||||
if (row.install_kind === "install" && (!installs.has(row.install_id) || at < installs.get(row.install_id))) installs.set(row.install_id, at);
|
||||
} else if (row.event === "session_start") {
|
||||
if (!["interactive", "resume", "fork", "api", "unknown", "ci", "fleet_worker", "agent", "scripted"].includes(row.session_source)) throw new Error("invalid session source");
|
||||
if (["ci", "fleet_worker", "agent", "scripted"].includes(row.session_source)) ci.add(row.install_id);
|
||||
if (!["interactive", "resume", "fork", "api", "unknown", "ci"].includes(row.session_source)) throw new Error("invalid session source");
|
||||
if (row.session_source === "ci") ci.add(row.install_id);
|
||||
started.add(row.install_id);
|
||||
} else if (row.event === "model_usage") {
|
||||
if (![4, 6].includes(count(row.schema_version, "schema_version"))) throw new Error("model usage requires v4 or v6");
|
||||
if (count(row.schema_version, "schema_version") !== 4) throw new Error("model usage requires v4");
|
||||
if (count(row.turns_ok, "turns_ok") > 0) {
|
||||
const observations = successes.get(row.install_id) ?? []; observations.push(at); successes.set(row.install_id, observations);
|
||||
}
|
||||
|
||||
@@ -24,14 +24,7 @@
|
||||
*/
|
||||
|
||||
import type { Batch, Event } from "./schema";
|
||||
import {
|
||||
CONTEXT_FIELDS,
|
||||
COUNTER_FIELDS,
|
||||
ERROR_FIELDS,
|
||||
MODEL_COUNTER_FIELDS,
|
||||
MODEL_COUNTER_FIELDS_V6_EXTRA,
|
||||
TURN_WALL_FIELDS,
|
||||
} from "./schema";
|
||||
import { COUNTER_FIELDS, ERROR_FIELDS, MODEL_COUNTER_FIELDS, TURN_WALL_FIELDS } from "./schema";
|
||||
|
||||
/** The subset of `AnalyticsEngineDataset` this Worker uses. */
|
||||
export interface DataPointSink {
|
||||
@@ -65,7 +58,7 @@ export const BLOB_COLUMNS = [
|
||||
"install_kind", // blob9
|
||||
"previous_version", // blob10
|
||||
"session_source", // blob11
|
||||
"duration_bucket", // blob12 session_end; install_age on session_start (schema 6)
|
||||
"duration_bucket", // blob12
|
||||
"exit_class", // blob13
|
||||
"cold_start_bucket", // blob14
|
||||
"providers", // blob15 comma-joined, already sorted and deduplicated
|
||||
@@ -73,7 +66,7 @@ export const BLOB_COLUMNS = [
|
||||
"sent_at", // blob17 the batch timestamp; events carry none
|
||||
"aggregate_counters", // blob18 closed product/operations JSON count object
|
||||
"schema_version", // blob19
|
||||
"privacy_version", // blob20: v3 to v6 notice, v2 consent, empty on v1; blob19 disambiguates
|
||||
"privacy_version", // blob20: v3/v4 notice, v2 consent, empty on v1; blob19 disambiguates
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -118,7 +111,6 @@ function toDataPoint(batch: Batch, event: Event): DataPoint {
|
||||
break;
|
||||
case "session_start":
|
||||
blobs[10] = event.source;
|
||||
blobs[11] = event.install_age ?? "";
|
||||
break;
|
||||
case "session_end":
|
||||
blobs[11] = event.duration_bucket;
|
||||
@@ -149,10 +141,8 @@ function toDataPoint(batch: Batch, event: Event): DataPoint {
|
||||
doubles = [
|
||||
...MODEL_COUNTER_FIELDS.map((field) => event.counters[field]),
|
||||
...TURN_WALL_FIELDS.map((field) => event.turn_wall[field]),
|
||||
...MODEL_COUNTER_FIELDS_V6_EXTRA.map((field) => event.counters[field] ?? 0),
|
||||
...CONTEXT_FIELDS.map((field) => event.context?.[field] ?? 0),
|
||||
...EMPTY_DOUBLES.slice(MODEL_COUNTER_FIELDS.length + TURN_WALL_FIELDS.length),
|
||||
];
|
||||
doubles = [...doubles, ...EMPTY_DOUBLES.slice(doubles.length)];
|
||||
break;
|
||||
case "tool_usage":
|
||||
blobs[15] = event.tool;
|
||||
|
||||
@@ -22,8 +22,6 @@ export const SCHEMA_VERSION = 4;
|
||||
/** Notice-v5 batches without `model_usage`, `tool_usage` or the `ci` source. */
|
||||
export const V3_SCHEMA_VERSION = 3;
|
||||
|
||||
export const V6_SCHEMA_VERSION = 6;
|
||||
|
||||
export const FUNNEL_SCHEMA_VERSION = 5;
|
||||
export const FUNNEL_NOTICE_VERSION = 7;
|
||||
export const FUNNEL_COUNTER_FIELDS = [
|
||||
@@ -36,8 +34,6 @@ export const FUNNEL_COUNTER_FIELDS = [
|
||||
/** Disclosed default-on policy, not a record of human acceptance. */
|
||||
export const NOTICE_VERSION = 6;
|
||||
|
||||
export const V6_NOTICE_VERSION = 8;
|
||||
|
||||
/** The notice version schema v3 batches carry. */
|
||||
export const V3_NOTICE_VERSION = 5;
|
||||
|
||||
@@ -150,11 +146,6 @@ export const SESSION_SOURCES = [
|
||||
"ci",
|
||||
] as const;
|
||||
|
||||
export const V6_ONLY_SESSION_SOURCES = ["fleet_worker", "agent", "scripted"] as const;
|
||||
export const V6_SESSION_SOURCES = [...SESSION_SOURCES, ...V6_ONLY_SESSION_SOURCES] as const;
|
||||
|
||||
export const INSTALL_AGES = ["new_today", "under_week", "older"] as const;
|
||||
|
||||
/** `DurationBucket` wire spellings. */
|
||||
export const DURATION_BUCKETS = [
|
||||
"lt_1m",
|
||||
@@ -166,8 +157,6 @@ export const DURATION_BUCKETS = [
|
||||
/** `ExitClass::as_str`. */
|
||||
export const EXIT_CLASSES = ["clean", "signal", "panic", "error"] as const;
|
||||
|
||||
export const V6_EXIT_CLASSES = [...EXIT_CLASSES, "unclean"] as const;
|
||||
|
||||
/** `ColdStartBucket` wire spellings. */
|
||||
export const COLD_START_BUCKETS = [
|
||||
"lt_250",
|
||||
@@ -222,19 +211,6 @@ export const MODEL_COUNTER_FIELDS = [
|
||||
"cache_write_tokens",
|
||||
] as const;
|
||||
|
||||
export const MODEL_COUNTER_FIELDS_V6_EXTRA = [
|
||||
"requests",
|
||||
"reasoning_tokens",
|
||||
"generation_secs",
|
||||
] as const;
|
||||
|
||||
export const V6_MODEL_COUNTER_FIELDS = [
|
||||
...MODEL_COUNTER_FIELDS,
|
||||
...MODEL_COUNTER_FIELDS_V6_EXTRA,
|
||||
] as const;
|
||||
|
||||
export const CONTEXT_FIELDS = ["lt_32k", "32k_128k", "128k_256k", "gte_256k"] as const;
|
||||
|
||||
/** `event::TOOL_NAMES` plus the `mcp` and `other` buckets. */
|
||||
export const TOOL_NAMES = [
|
||||
"agent", "apply_patch", "authenticate", "bash", "code_execution", "create_goal",
|
||||
@@ -310,12 +286,6 @@ export const EVENT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
||||
funnel_usage: ["event", "counters"],
|
||||
};
|
||||
|
||||
export const V6_EVENT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
||||
...EVENT_FIELDS,
|
||||
session_start: ["event", "source", "install_age"],
|
||||
model_usage: ["event", "provider", "model", "counters", "turn_wall", "context"],
|
||||
};
|
||||
|
||||
/** Every event discriminant, for the doc-match test. */
|
||||
export const EVENT_NAMES = Object.keys(EVENT_FIELDS);
|
||||
|
||||
@@ -460,7 +430,7 @@ export type FunnelCounters = Record<(typeof FUNNEL_COUNTER_FIELDS)[number], numb
|
||||
|
||||
export type Event =
|
||||
| { event: "install_or_upgrade"; kind: string; previous_version: string | null }
|
||||
| { event: "session_start"; source: string; install_age?: string | null }
|
||||
| { event: "session_start"; source: string }
|
||||
| {
|
||||
event: "session_end";
|
||||
duration_bucket: string;
|
||||
@@ -478,10 +448,8 @@ export type Event =
|
||||
event: "model_usage";
|
||||
provider: string;
|
||||
model: string;
|
||||
counters: Record<(typeof MODEL_COUNTER_FIELDS)[number], number> &
|
||||
Partial<Record<(typeof MODEL_COUNTER_FIELDS_V6_EXTRA)[number], number>>;
|
||||
counters: Record<(typeof MODEL_COUNTER_FIELDS)[number], number>;
|
||||
turn_wall: TurnWall;
|
||||
context?: Record<(typeof CONTEXT_FIELDS)[number], number>;
|
||||
}
|
||||
| { event: "tool_usage"; tool: string; calls: number; errors: number }
|
||||
| ({ event: "operations_summary" } & Record<(typeof OPERATIONS_FIELDS)[number], number>);
|
||||
@@ -578,28 +546,13 @@ function u32Map(
|
||||
return null;
|
||||
}
|
||||
|
||||
function u32Fields(
|
||||
value: Record<string, unknown>,
|
||||
fields: readonly string[],
|
||||
where: string,
|
||||
): string | null {
|
||||
for (const field of fields) {
|
||||
const item = value[field];
|
||||
if (typeof item !== "number" || !Number.isInteger(item) || item < 0 || item > U32_MAX) {
|
||||
return `${where}.${field}: not a u32`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEvent(value: unknown, where: string, v6 = false): string | null {
|
||||
function validateEvent(value: unknown, where: string): string | null {
|
||||
if (!isPlainObject(value)) return `${where}: not an object`;
|
||||
const name = value.event;
|
||||
const eventFields = v6 ? V6_EVENT_FIELDS : EVENT_FIELDS;
|
||||
if (typeof name !== "string" || !Object.hasOwn(eventFields, name)) {
|
||||
if (typeof name !== "string" || !Object.hasOwn(EVENT_FIELDS, name)) {
|
||||
return `${where}: unknown event discriminant`;
|
||||
}
|
||||
const keyError = keysExactly(value, eventFields[name], `${where}(${name})`);
|
||||
const keyError = keysExactly(value, EVENT_FIELDS[name], `${where}(${name})`);
|
||||
if (keyError) return keyError;
|
||||
|
||||
switch (name) {
|
||||
@@ -622,15 +575,8 @@ function validateEvent(value: unknown, where: string, v6 = false): string | null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case "session_start": {
|
||||
const sourceError = enumString(
|
||||
value.source,
|
||||
v6 ? V6_SESSION_SOURCES : SESSION_SOURCES,
|
||||
`${where}.source`,
|
||||
);
|
||||
if (sourceError || !v6 || value.install_age === null) return sourceError;
|
||||
return enumString(value.install_age, INSTALL_AGES, `${where}.install_age`);
|
||||
}
|
||||
case "session_start":
|
||||
return enumString(value.source, SESSION_SOURCES, `${where}.source`);
|
||||
case "session_end": {
|
||||
const bucketError = enumString(
|
||||
value.duration_bucket,
|
||||
@@ -640,7 +586,7 @@ function validateEvent(value: unknown, where: string, v6 = false): string | null
|
||||
if (bucketError) return bucketError;
|
||||
const exitError = enumString(
|
||||
value.exit_class,
|
||||
v6 ? V6_EXIT_CLASSES : EXIT_CLASSES,
|
||||
EXIT_CLASSES,
|
||||
`${where}.exit_class`,
|
||||
);
|
||||
if (exitError) return exitError;
|
||||
@@ -692,14 +638,9 @@ function validateEvent(value: unknown, where: string, v6 = false): string | null
|
||||
if (typeof value.model !== "string" || !MODEL_RE.test(value.model)) {
|
||||
return `${where}.model: not a catalogued model id`;
|
||||
}
|
||||
const counterError = v6
|
||||
? countMap(value.counters, V6_MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER) ??
|
||||
u32Fields(value.counters as Record<string, unknown>, ["requests", "generation_secs"], `${where}.counters`)
|
||||
: countMap(value.counters, MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER);
|
||||
return (
|
||||
counterError ??
|
||||
u32Map(value.turn_wall, TURN_WALL_FIELDS, `${where}.turn_wall`) ??
|
||||
(v6 ? u32Map(value.context, CONTEXT_FIELDS, `${where}.context`) : null)
|
||||
countMap(value.counters, MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER) ??
|
||||
u32Map(value.turn_wall, TURN_WALL_FIELDS, `${where}.turn_wall`)
|
||||
);
|
||||
}
|
||||
case "tool_usage":
|
||||
@@ -734,12 +675,15 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
|
||||
const legacy = value.schema_version === 1;
|
||||
const explicitConsent = value.schema_version === 2;
|
||||
const v3 = value.schema_version === V3_SCHEMA_VERSION;
|
||||
const v4 = value.schema_version === SCHEMA_VERSION;
|
||||
const funnel = value.schema_version === FUNNEL_SCHEMA_VERSION;
|
||||
const v6 = value.schema_version === V6_SCHEMA_VERSION;
|
||||
const preUsage = legacy || explicitConsent || v3;
|
||||
if (!legacy && !explicitConsent && !v3 && !v4 && !funnel && !v6) {
|
||||
const preUsage = value.schema_version !== SCHEMA_VERSION && !funnel;
|
||||
if (
|
||||
!legacy &&
|
||||
!explicitConsent &&
|
||||
value.schema_version !== SCHEMA_VERSION &&
|
||||
!funnel &&
|
||||
value.schema_version !== V3_SCHEMA_VERSION
|
||||
) {
|
||||
return { ok: false, reason: "batch.schema_version: unsupported" };
|
||||
}
|
||||
const fields = legacy ? LEGACY_ENVELOPE_FIELDS : explicitConsent ? V2_ENVELOPE_FIELDS : ENVELOPE_FIELDS;
|
||||
@@ -751,7 +695,7 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
if (
|
||||
!legacy &&
|
||||
!explicitConsent &&
|
||||
value.notice_version !== (funnel ? FUNNEL_NOTICE_VERSION : v6 ? V6_NOTICE_VERSION : preUsage ? V3_NOTICE_VERSION : NOTICE_VERSION)
|
||||
value.notice_version !== (funnel ? FUNNEL_NOTICE_VERSION : preUsage ? V3_NOTICE_VERSION : NOTICE_VERSION)
|
||||
) {
|
||||
return { ok: false, reason: "batch.notice_version: current policy required" };
|
||||
}
|
||||
@@ -809,7 +753,7 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
return { ok: false, reason: "events: aggregate product/operations events require schema v2 or v3" };
|
||||
}
|
||||
if (preUsage && (event.event === "model_usage" || event.event === "tool_usage")) {
|
||||
return { ok: false, reason: "events: model_usage and tool_usage require schema v4 or later" };
|
||||
return { ok: false, reason: "events: model_usage and tool_usage require schema v4" };
|
||||
}
|
||||
if (preUsage && event.event === "session_start" && event.source === "ci") {
|
||||
return { ok: false, reason: "events: the ci session source requires schema v4" };
|
||||
@@ -818,7 +762,7 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
return { ok: false, reason: "events: operations_summary requires control-plane surface" };
|
||||
}
|
||||
}
|
||||
const eventError = validateEvent(event, `events[${index}]`, v6);
|
||||
const eventError = validateEvent(event, `events[${index}]`);
|
||||
if (eventError) return { ok: false, reason: eventError };
|
||||
}
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ describe("additive website funnel contract", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the historical goldens and v6 accepted, with runtime v4 and v6 first-party only", async () => {
|
||||
it("keeps all four historical goldens accepted and runtime v4 first-party only", async () => {
|
||||
const fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
|
||||
for (const version of [1, 2, 3, 4, 6]) {
|
||||
for (const version of [1, 2, 3, 4]) {
|
||||
const batch = JSON.parse(readFileSync(new URL(`../../crates/telemetry/tests/golden/v${version}.json`, import.meta.url), "utf8"));
|
||||
expect(validateBatch(batch).ok, `v${version}`).toBe(true);
|
||||
if (version !== 4 && version !== 6) continue;
|
||||
if (version !== 4) continue;
|
||||
const { env, written } = harness();
|
||||
expect((await worker.fetch(postJson(batch), { ...env, POSTHOG_HOST: "https://us.i.posthog.com", POSTHOG_PROJECT_TOKEN: "phc_local_test_fixture", POSTHOG_IP_SAFE_EGRESS_VERIFIED: "true" })).status).toBe(204);
|
||||
expect(written).toHaveLength(batch.events.length);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
|
||||
import worker from "../src/index";
|
||||
import { INGEST_PATH } from "../src/route";
|
||||
import { BLOB_COLUMNS, DOUBLE_COLUMNS } from "../src/datapoint";
|
||||
import { MAX_BODY_BYTES, SURFACES, validateBatch } from "../src/schema";
|
||||
import { MAX_BODY_BYTES, SURFACES } from "../src/schema";
|
||||
import { goldenBatch, harness, post, postJson } from "./support";
|
||||
|
||||
describe("method and route", () => {
|
||||
@@ -473,149 +473,3 @@ describe("failing closed and quiet", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema 6", () => {
|
||||
const golden = (version: number): Record<string, any> =>
|
||||
JSON.parse(
|
||||
readFileSync(
|
||||
new URL(`../../crates/telemetry/tests/golden/v${version}.json`, import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
const event = (batch: Record<string, any>, name: string) =>
|
||||
batch.events.find((item: Record<string, any>) => item.event === name);
|
||||
|
||||
it("accepts every historical golden and the v6 golden", () => {
|
||||
for (const version of [1, 2, 3, 4, 6]) {
|
||||
expect(validateBatch(golden(version)).ok, `v${version}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("answers 204 to the v4 and v6 goldens", async () => {
|
||||
for (const version of [4, 6]) {
|
||||
const { env, written } = harness();
|
||||
expect((await worker.fetch(postJson(golden(version)), env)).status).toBe(204);
|
||||
expect(written).toHaveLength(golden(version).events.length);
|
||||
}
|
||||
});
|
||||
|
||||
it("requires notice 8 on v6 and refuses the v4 notice", () => {
|
||||
const batch = golden(6);
|
||||
batch.notice_version = 6;
|
||||
expect(validateBatch(batch).ok).toBe(false);
|
||||
expect(validateBatch({ ...golden(4), notice_version: 8 }).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses a v4 batch carrying any v6 key or value", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { event(batch, "model_usage").context = event(golden(6), "model_usage").context; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 1; },
|
||||
(batch) => { event(batch, "model_usage").counters.reasoning_tokens = 1; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = 1; },
|
||||
(batch) => { event(batch, "session_start").install_age = "older"; },
|
||||
(batch) => { event(batch, "session_start").source = "fleet_worker"; },
|
||||
(batch) => { event(batch, "session_start").source = "agent"; },
|
||||
(batch) => { event(batch, "session_start").source = "scripted"; },
|
||||
(batch) => { event(batch, "session_end").exit_class = "unclean"; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(4);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses a v6 batch missing or adding a key", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { delete event(batch, "model_usage").context; },
|
||||
(batch) => { delete event(batch, "model_usage").context["32k_128k"]; },
|
||||
(batch) => { event(batch, "model_usage").context.gte_512k = 0; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.requests; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.reasoning_tokens; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.generation_secs; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_ms = 1; },
|
||||
(batch) => { event(batch, "model_usage").model_family = "x"; },
|
||||
(batch) => { delete event(batch, "session_start").install_age; },
|
||||
(batch) => { event(batch, "session_start").extra = "x"; },
|
||||
(batch) => { event(batch, "session_end").counters.requests = 1; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(6);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses open values where v6 has a closed set or a count", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { event(batch, "session_start").install_age = "2026-11-01"; },
|
||||
(batch) => { event(batch, "session_start").install_age = "OLDER"; },
|
||||
(batch) => { event(batch, "session_start").source = "github-actions"; },
|
||||
(batch) => { event(batch, "session_end").exit_class = "killed"; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 4294967296; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 1.5; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = -1; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = 4294967296; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = 1794528251123; },
|
||||
(batch) => { event(batch, "model_usage").counters.reasoning_tokens = "7"; },
|
||||
(batch) => { event(batch, "model_usage").context.lt_32k = 4294967296; },
|
||||
(batch) => { event(batch, "model_usage").context.lt_32k = "1"; },
|
||||
(batch) => { event(batch, "model_usage").model = "/Users/someone/model"; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(6);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a null install_age and every v6 source and exit class", () => {
|
||||
for (const source of ["interactive", "resume", "fork", "api", "unknown", "ci", "fleet_worker", "agent", "scripted"]) {
|
||||
for (const age of ["new_today", "under_week", "older", null]) {
|
||||
const batch = golden(6);
|
||||
event(batch, "session_start").source = source;
|
||||
event(batch, "session_start").install_age = age;
|
||||
expect(validateBatch(batch).ok, `${source}/${age}`).toBe(true);
|
||||
}
|
||||
}
|
||||
for (const exit of ["clean", "signal", "panic", "error", "unclean"]) {
|
||||
const batch = golden(6);
|
||||
event(batch, "session_end").exit_class = exit;
|
||||
expect(validateBatch(batch).ok, exit).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("writes v6 model_usage after the unchanged eleven doubles, and leaves v4 rows zero there", async () => {
|
||||
const v6 = harness();
|
||||
await worker.fetch(postJson(golden(6)), v6.env);
|
||||
const row = v6.written.find((point) => point.blobs[0] === "model_usage")!;
|
||||
expect(row.doubles).toEqual([
|
||||
12, 1, 0, 418230, 9120, 380001, 0,
|
||||
3, 8, 2, 0,
|
||||
27, 2310, 412,
|
||||
9, 11, 5, 2,
|
||||
0, 0,
|
||||
]);
|
||||
expect(row.blobs[BLOB_COLUMNS.indexOf("schema_version")]).toBe("6");
|
||||
|
||||
const v4 = harness();
|
||||
await worker.fetch(postJson(golden(4)), v4.env);
|
||||
const old = v4.written.find((point) => point.blobs[0] === "model_usage")!;
|
||||
expect(old.doubles).toEqual([
|
||||
12, 1, 0, 418230, 9120, 380001, 0,
|
||||
3, 8, 2, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]);
|
||||
});
|
||||
|
||||
it("writes install_age to blob12 of session_start rows only", async () => {
|
||||
const { env, written } = harness();
|
||||
await worker.fetch(postJson(golden(6)), env);
|
||||
const start = written.find((point) => point.blobs[0] === "session_start")!;
|
||||
expect(start.blobs[BLOB_COLUMNS.indexOf("session_source")]).toBe("fleet_worker");
|
||||
expect(start.blobs[BLOB_COLUMNS.indexOf("duration_bucket")]).toBe("under_week");
|
||||
const end = written.find((point) => point.blobs[0] === "session_end")!;
|
||||
expect(end.blobs[BLOB_COLUMNS.indexOf("exit_class")]).toBe("unclean");
|
||||
expect(end.blobs[BLOB_COLUMNS.indexOf("duration_bucket")]).toBe("1m_10m");
|
||||
});
|
||||
});
|
||||
@@ -49,19 +49,6 @@ describe("existing-pipeline owner funnel report", () => {
|
||||
expect(buildFunnelReport(input, { now: NOW }).native.seven_day_initialization_cohort.unavailable_reason).toContain("incomplete");
|
||||
});
|
||||
|
||||
it("excludes fleet worker, agent and scripted starts like CI, and accepts v6 model usage", () => {
|
||||
for (const source of ["fleet_worker", "agent", "scripted"]) {
|
||||
const input = receipt();
|
||||
input.native_rows.push({ ...native("session_start", "2026-10-04T00:00:00Z"), session_source: source });
|
||||
input.native_rows.push({ ...native("model_usage", "2026-10-04T00:00:00Z"), schema_version: 6 });
|
||||
const report = buildFunnelReport(input, { now: NOW });
|
||||
expect(report.native.excluded_ci_ids, source).toBe(1);
|
||||
expect(report.native.observed_completed_ids, source).toBe(0);
|
||||
}
|
||||
const input = receipt(); input.native_rows.push({ ...native("model_usage", "2026-10-04T00:00:00Z"), schema_version: 5 });
|
||||
expect(() => buildFunnelReport(input, { now: NOW })).toThrow(/version/);
|
||||
});
|
||||
|
||||
it("does not compute a cohort fraction from sampled native records", () => {
|
||||
const input = receipt(); input.native_rows[0].weight = 4;
|
||||
expect(buildFunnelReport(input, { now: NOW }).native.seven_day_initialization_cohort).toMatchObject({ fraction: null, sampled: true });
|
||||
|
||||
@@ -20,27 +20,23 @@ import {
|
||||
BATCH_MAX_BYTES,
|
||||
BATCH_MAX_EVENTS,
|
||||
COLD_START_BUCKETS,
|
||||
CONTEXT_FIELDS,
|
||||
COUNTER_FIELDS,
|
||||
DURATION_BUCKETS,
|
||||
ENVELOPE_FIELDS,
|
||||
ERROR_FIELDS,
|
||||
EVENT_FIELDS,
|
||||
EVENT_NAMES,
|
||||
INSTALL_AGES,
|
||||
EXIT_CLASSES,
|
||||
INSTALL_KINDS,
|
||||
LIBCS,
|
||||
MAX_BODY_BYTES,
|
||||
OSES,
|
||||
PRODUCT_COUNTER_FIELDS,
|
||||
OPERATIONS_FIELDS,
|
||||
SCHEMA_VERSION,
|
||||
SESSION_SOURCES,
|
||||
SURFACES,
|
||||
TURN_WALL_FIELDS,
|
||||
V6_EVENT_FIELDS as EVENT_FIELDS,
|
||||
V6_EXIT_CLASSES as EXIT_CLASSES,
|
||||
V6_MODEL_COUNTER_FIELDS,
|
||||
V6_NOTICE_VERSION,
|
||||
V6_SCHEMA_VERSION as SCHEMA_VERSION,
|
||||
V6_SESSION_SOURCES as SESSION_SOURCES,
|
||||
} from "../src/schema";
|
||||
import { DOC, goldenBatch } from "./support";
|
||||
|
||||
@@ -172,16 +168,6 @@ describe("the doc and the validator publish the same fields", () => {
|
||||
expect([...documentedKeys(BLOCKS[3])].sort()).toEqual(expected);
|
||||
});
|
||||
|
||||
it("agrees on the model_usage field set, nested objects included", () => {
|
||||
const expected = [
|
||||
...EVENT_FIELDS.model_usage,
|
||||
...V6_MODEL_COUNTER_FIELDS,
|
||||
...TURN_WALL_FIELDS,
|
||||
...CONTEXT_FIELDS,
|
||||
].sort();
|
||||
expect([...documentedKeys(BLOCKS[7])].sort()).toEqual([...new Set(expected)].sort());
|
||||
});
|
||||
|
||||
it("agrees on the panic field set", () => {
|
||||
expect([...documentedKeys(BLOCKS[4])].sort()).toEqual(
|
||||
[...EVENT_FIELDS.panic].sort(),
|
||||
@@ -230,7 +216,6 @@ describe("the doc and the validator publish the same enums", () => {
|
||||
["session source", SESSION_SOURCES],
|
||||
["duration bucket", DURATION_BUCKETS],
|
||||
["exit class", EXIT_CLASSES],
|
||||
["install age", INSTALL_AGES],
|
||||
["cold start bucket", COLD_START_BUCKETS],
|
||||
] as const)("publishes every %s value it accepts", (_label, values) => {
|
||||
for (const value of values) {
|
||||
@@ -247,8 +232,6 @@ describe("the doc and the validator publish the same limits", () => {
|
||||
expect(match).not.toBeNull();
|
||||
expect(Number((match as RegExpMatchArray)[1])).toBe(SCHEMA_VERSION);
|
||||
expect(goldenBatch().schema_version).toBe(1); // preserved legacy golden
|
||||
const notice = DOC.match(/`notice_version\s*=\s*(\d+)`/);
|
||||
expect(Number((notice as RegExpMatchArray)[1])).toBe(V6_NOTICE_VERSION);
|
||||
});
|
||||
|
||||
it("agrees on the per-batch caps", () => {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
@@ -36,8 +36,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -52,8 +52,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/KEYBINDINGS.md",
|
||||
@@ -68,8 +68,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/PROVIDERS.md",
|
||||
@@ -387,8 +387,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/OPERATIONS_RUNBOOK.md",
|
||||
|
||||
Reference in new issue
Block a user