agentwrangler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +116 -0
- package/dist/apply/jobs.js +429 -0
- package/dist/apply/open-terminal-child.mjs +98 -0
- package/dist/apply/open-terminal.js +221 -0
- package/dist/apply/settings-gen.js +35 -0
- package/dist/cli/agentwrangler.js +18 -0
- package/dist/daemon/config.js +51 -0
- package/dist/daemon/http.js +258 -0
- package/dist/daemon/index.js +372 -0
- package/dist/daemon/outcomes-pass.js +82 -0
- package/dist/daemon/readiness.js +15 -0
- package/dist/daemon/router.js +756 -0
- package/dist/daemon/static.js +146 -0
- package/dist/db/migrate.js +72 -0
- package/dist/db/migrations/001_observe.sql +196 -0
- package/dist/db/migrations/002_indexes.sql +6 -0
- package/dist/db/migrations/003_context_inventory_history.sql +20 -0
- package/dist/db/migrations/004_apply_jobs.sql +17 -0
- package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
- package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
- package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
- package/dist/db/migrations/008_thinking_tokens.sql +1 -0
- package/dist/db/migrations/009_user_turn_count.sql +1 -0
- package/dist/db/migrations/010_workspace_cwd.sql +1 -0
- package/dist/db/migrations/011_reports.sql +1 -0
- package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
- package/dist/db/migrations/013_friction_fields.sql +5 -0
- package/dist/db/migrations/014_session_churn.sql +11 -0
- package/dist/db/migrations/015_gap_aggregates.sql +6 -0
- package/dist/db/open.js +30 -0
- package/dist/detector/benchmark-anchors.js +36 -0
- package/dist/detector/calibration.js +302 -0
- package/dist/detector/context-history-retention.js +312 -0
- package/dist/detector/context-probe.js +574 -0
- package/dist/detector/d1-source-identity.js +25 -0
- package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
- package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
- package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
- package/dist/detector/detectors/d4_model_mismatch.js +258 -0
- package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
- package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
- package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
- package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
- package/dist/detector/detectors/d9_idle_background_session.js +101 -0
- package/dist/detector/engine.js +88 -0
- package/dist/detector/index.js +17 -0
- package/dist/detector/measurement.js +426 -0
- package/dist/detector/practice-registry.js +259 -0
- package/dist/detector/registry.js +32 -0
- package/dist/detector/savings.js +249 -0
- package/dist/detector/types.js +14 -0
- package/dist/evidence/common/approved-input.js +632 -0
- package/dist/evidence/common/boundary.js +84 -0
- package/dist/evidence/common/canonical.js +55 -0
- package/dist/evidence/common/redaction.js +321 -0
- package/dist/evidence/common/sqlite.js +25 -0
- package/dist/evidence/common/state.js +29 -0
- package/dist/evidence/cond1/cli.js +289 -0
- package/dist/evidence/cond1/packet.js +407 -0
- package/dist/evidence/cond1/prepare.js +295 -0
- package/dist/evidence/cond1/score.js +349 -0
- package/dist/evidence/cond1/types.js +1 -0
- package/dist/evidence/create-approval.js +365 -0
- package/dist/evidence/create-scratch.js +542 -0
- package/dist/evidence/d7/cli.js +113 -0
- package/dist/evidence/d7/measure.js +193 -0
- package/dist/evidence/d7/types.js +1 -0
- package/dist/evidence/discover-approval.js +492 -0
- package/dist/evidence/g2/adjudicate.js +20 -0
- package/dist/evidence/g2/cli.js +207 -0
- package/dist/evidence/g2/kappa.js +39 -0
- package/dist/evidence/g2/pipeline.js +92 -0
- package/dist/evidence/g2/store.js +14 -0
- package/dist/evidence/github/client.js +1 -0
- package/dist/evidence/github/gh-cli-client.js +301 -0
- package/dist/evidence/r3/cli.js +209 -0
- package/dist/evidence/r3/evaluate.js +417 -0
- package/dist/evidence/r3/packet.js +162 -0
- package/dist/evidence/r3/prepare.js +405 -0
- package/dist/evidence/r3/score.js +341 -0
- package/dist/evidence/r3/transcript.js +155 -0
- package/dist/evidence/r3/types.js +4 -0
- package/dist/hook/context-budget-hook.mjs +138 -0
- package/dist/hook/danger-guard-denylist.json +27 -0
- package/dist/hook/danger-guard-hook.mjs +167 -0
- package/dist/hook/install.js +0 -0
- package/dist/hook/limit-burn-hook.mjs +127 -0
- package/dist/hook/loop-guard-hook.mjs +104 -0
- package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
- package/dist/ingest/churn-collector.js +122 -0
- package/dist/ingest/detector-hook.js +52 -0
- package/dist/ingest/discovery.js +207 -0
- package/dist/ingest/health.js +43 -0
- package/dist/ingest/index.js +28 -0
- package/dist/ingest/ingestor.js +509 -0
- package/dist/ingest/parser.js +344 -0
- package/dist/ingest/pricing.js +153 -0
- package/dist/ingest/reconcile.js +52 -0
- package/dist/ingest/tail.js +152 -0
- package/dist/ingest/types.js +24 -0
- package/dist/ingest/workspace-mapping.js +114 -0
- package/dist/oauth/anthropic-api-key.js +88 -0
- package/dist/oauth/count-tokens.js +86 -0
- package/dist/oauth/credentials.js +171 -0
- package/dist/oauth/judge-g2-client.js +154 -0
- package/dist/oauth/usage.js +167 -0
- package/dist/outcomes/branch-key.js +49 -0
- package/dist/outcomes/conclusions.js +45 -0
- package/dist/outcomes/derive.js +94 -0
- package/dist/outcomes/finding-extractors.js +131 -0
- package/dist/outcomes/findings.js +237 -0
- package/dist/outcomes/github/client.js +367 -0
- package/dist/outcomes/github/credential.js +195 -0
- package/dist/outcomes/github/gh-cli-client.js +340 -0
- package/dist/outcomes/linker.js +486 -0
- package/dist/outcomes/pool.js +24 -0
- package/dist/outcomes/sync.js +276 -0
- package/dist/query/api/agents-liveness.js +182 -0
- package/dist/query/api/burn-status.js +50 -0
- package/dist/query/api/context-budget.js +114 -0
- package/dist/query/api/context-composition.js +67 -0
- package/dist/query/api/cost-per-success.js +104 -0
- package/dist/query/api/delivery.js +92 -0
- package/dist/query/api/effectiveness.js +254 -0
- package/dist/query/api/efficiency-headroom.js +74 -0
- package/dist/query/api/headroom-trend.js +105 -0
- package/dist/query/api/hook-config.js +75 -0
- package/dist/query/api/hook-install.js +8 -0
- package/dist/query/api/hot-sessions.js +17 -0
- package/dist/query/api/idle-sessions.js +52 -0
- package/dist/query/api/index.js +40 -0
- package/dist/query/api/loop-guard.js +90 -0
- package/dist/query/api/offload-share.js +41 -0
- package/dist/query/api/outcomes.js +218 -0
- package/dist/query/api/overview.js +535 -0
- package/dist/query/api/rec-prompt.js +138 -0
- package/dist/query/api/recommendations-ledger.js +111 -0
- package/dist/query/api/recommendations.js +514 -0
- package/dist/query/api/reports.js +78 -0
- package/dist/query/api/self-churn.js +77 -0
- package/dist/query/api/self-percentiles.js +109 -0
- package/dist/query/api/session-drivers.js +153 -0
- package/dist/query/api/settings.js +85 -0
- package/dist/query/api/spend-flavor.js +234 -0
- package/dist/query/api/trends.js +155 -0
- package/dist/query/cap-weighted.js +119 -0
- package/dist/query/db-context.js +42 -0
- package/dist/query/envelope.js +71 -0
- package/dist/query/forecast.js +191 -0
- package/dist/query/settings-store.js +441 -0
- package/dist/query/spend.js +171 -0
- package/dist/query/trends.js +194 -0
- package/dist/ui/assets/index-DnRKgc21.css +1 -0
- package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
- package/dist/ui/index.html +39 -0
- package/package.json +59 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Runtime configuration for the local context-budget hook. */
|
|
2
|
+
import { isHookInstalled } from "../../hook/install.js";
|
|
3
|
+
import { getQueryDb } from "../db-context.js";
|
|
4
|
+
import { buildResponse } from "../envelope.js";
|
|
5
|
+
export const DEFAULT_HOOK_CONFIG = {
|
|
6
|
+
context_window: 200_000,
|
|
7
|
+
soft_pct: 0.6,
|
|
8
|
+
hard_pct: 0.8,
|
|
9
|
+
stale_s: 300,
|
|
10
|
+
d7_fail_count: 3,
|
|
11
|
+
d7_window_turns: 10,
|
|
12
|
+
d9_idle_seconds: 1800,
|
|
13
|
+
};
|
|
14
|
+
const CONFIG_PREFIX = "hook_config.";
|
|
15
|
+
const CONFIG_KEYS = Object.keys(DEFAULT_HOOK_CONFIG);
|
|
16
|
+
function configKey(key) {
|
|
17
|
+
return `${CONFIG_PREFIX}${key}`;
|
|
18
|
+
}
|
|
19
|
+
function isValidConfig(config) {
|
|
20
|
+
return (Number.isFinite(config.context_window) &&
|
|
21
|
+
config.context_window > 0 &&
|
|
22
|
+
Number.isFinite(config.soft_pct) &&
|
|
23
|
+
config.soft_pct > 0 &&
|
|
24
|
+
Number.isFinite(config.hard_pct) &&
|
|
25
|
+
config.hard_pct > config.soft_pct &&
|
|
26
|
+
config.hard_pct <= 1 &&
|
|
27
|
+
Number.isFinite(config.stale_s) &&
|
|
28
|
+
config.stale_s > 0 &&
|
|
29
|
+
Number.isInteger(config.d7_fail_count) &&
|
|
30
|
+
config.d7_fail_count >= 3 &&
|
|
31
|
+
Number.isInteger(config.d7_window_turns) &&
|
|
32
|
+
config.d7_window_turns > 0 &&
|
|
33
|
+
Number.isInteger(config.d9_idle_seconds) &&
|
|
34
|
+
config.d9_idle_seconds > 0);
|
|
35
|
+
}
|
|
36
|
+
/** Read each dedicated user_config key, degrading corrupt values to defaults. */
|
|
37
|
+
export function readHookConfig(db) {
|
|
38
|
+
const read = db.prepare("SELECT value FROM user_config WHERE key = ?");
|
|
39
|
+
const config = { ...DEFAULT_HOOK_CONFIG };
|
|
40
|
+
for (const key of CONFIG_KEYS) {
|
|
41
|
+
const row = read.get(configKey(key));
|
|
42
|
+
if (row?.value === null || row?.value === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const value = Number(row.value);
|
|
45
|
+
if (Number.isFinite(value))
|
|
46
|
+
config[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return isValidConfig(config) ? config : { ...DEFAULT_HOOK_CONFIG };
|
|
49
|
+
}
|
|
50
|
+
export function getHookConfig() {
|
|
51
|
+
return buildResponse({ ...readHookConfig(getQueryDb()), installed: isHookInstalled() }, { claim_kind: "N_A", n: 1 });
|
|
52
|
+
}
|
|
53
|
+
/** Validate and atomically persist a partial update to dedicated config keys. */
|
|
54
|
+
export function updateHookConfig(update) {
|
|
55
|
+
if (update === null || typeof update !== "object" || Array.isArray(update)) {
|
|
56
|
+
throw new Error("Invalid hook config");
|
|
57
|
+
}
|
|
58
|
+
for (const key of Object.keys(update)) {
|
|
59
|
+
if (!CONFIG_KEYS.includes(key))
|
|
60
|
+
throw new Error("Invalid hook config key");
|
|
61
|
+
}
|
|
62
|
+
const db = getQueryDb();
|
|
63
|
+
const next = { ...readHookConfig(db), ...update };
|
|
64
|
+
if (!isValidConfig(next))
|
|
65
|
+
throw new Error("Invalid hook config");
|
|
66
|
+
const write = db.prepare(`INSERT INTO user_config (key, value, updated_at) VALUES (?, ?, ?)
|
|
67
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);
|
|
68
|
+
db.transaction(() => {
|
|
69
|
+
for (const key of CONFIG_KEYS) {
|
|
70
|
+
if (update[key] !== undefined)
|
|
71
|
+
write.run(configKey(key), String(next[key]), new Date().toISOString());
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
return buildResponse(next, { claim_kind: "N_A", n: 1 });
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Local-only wrappers for the Claude Code hook installer. */
|
|
2
|
+
import { installHook, uninstallHook } from "../../hook/install.js";
|
|
3
|
+
export function installHookRoute() {
|
|
4
|
+
return installHook();
|
|
5
|
+
}
|
|
6
|
+
export function uninstallHookRoute() {
|
|
7
|
+
return uninstallHook();
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getQueryDb } from "../db-context.js";
|
|
2
|
+
import { hotSessionsByCost } from "../spend.js";
|
|
3
|
+
import { getSessionSpendPercentile } from "./self-percentiles.js";
|
|
4
|
+
export function getHotSessions(window, limit = 20) {
|
|
5
|
+
const db = getQueryDb();
|
|
6
|
+
// One query for the ranked rows, then one cheap percentile lookup per row
|
|
7
|
+
// (<= limit rows, so no N-per-row HTTP calls). Reuses the per-session query
|
|
8
|
+
// so the two BM3 surfaces stay identical.
|
|
9
|
+
return hotSessionsByCost(db, limit, window).map((row) => {
|
|
10
|
+
const percentile = getSessionSpendPercentile(db, row.session_id);
|
|
11
|
+
return {
|
|
12
|
+
...row,
|
|
13
|
+
spend_percentile: percentile.percentile,
|
|
14
|
+
spend_percentile_n: percentile.n,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Fail-open idle background-session measurements for the local dashboard. */
|
|
2
|
+
import { capWeightExprSql, resolveCapReadCoeff } from "../cap-weighted.js";
|
|
3
|
+
import { getQueryDb } from "../db-context.js";
|
|
4
|
+
import { buildResponse } from "../envelope.js";
|
|
5
|
+
import { readHookConfig } from "./hook-config.js";
|
|
6
|
+
function response(rows) {
|
|
7
|
+
return buildResponse(rows, { claim_kind: "OBS_PROXY", n: rows.length });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Return idle sessions whose reconciled cap-weighted usage is dominated by
|
|
11
|
+
* sidechain turns. This is display-only metadata: no session control occurs.
|
|
12
|
+
*/
|
|
13
|
+
export function getIdleSessions() {
|
|
14
|
+
try {
|
|
15
|
+
const db = getQueryDb();
|
|
16
|
+
const config = readHookConfig(db);
|
|
17
|
+
const expr = capWeightExprSql("t", resolveCapReadCoeff(db));
|
|
18
|
+
const rawRows = db
|
|
19
|
+
.prepare(`SELECT t.session_id AS session_id,
|
|
20
|
+
MAX(t.workspace_id) AS workspace_id,
|
|
21
|
+
MAX(t.ts) AS last_activity_ts,
|
|
22
|
+
SUM(${expr}) AS cap_weighted_raw
|
|
23
|
+
FROM turns t
|
|
24
|
+
WHERE t.provisional = 0
|
|
25
|
+
GROUP BY t.session_id
|
|
26
|
+
HAVING SUM(CASE WHEN t.is_sidechain = 1 THEN ${expr} ELSE 0 END)
|
|
27
|
+
> SUM(${expr}) / 2`)
|
|
28
|
+
.all();
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (const row of rawRows) {
|
|
32
|
+
const lastActivityMs = Date.parse(row.last_activity_ts);
|
|
33
|
+
if (!Number.isFinite(lastActivityMs))
|
|
34
|
+
continue;
|
|
35
|
+
const idleSeconds = Math.floor(Math.max(0, now - lastActivityMs) / 1_000);
|
|
36
|
+
if (idleSeconds < config.d9_idle_seconds)
|
|
37
|
+
continue;
|
|
38
|
+
rows.push({
|
|
39
|
+
session_id: row.session_id,
|
|
40
|
+
workspace_id: row.workspace_id,
|
|
41
|
+
last_activity_ts: row.last_activity_ts,
|
|
42
|
+
idle_seconds: idleSeconds,
|
|
43
|
+
cap_weighted_tokens: Math.round(row.cap_weighted_raw ?? 0),
|
|
44
|
+
sidechain: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return response(rows);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return response([]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/query/api/index.ts — LocalQueryAPI barrel.
|
|
3
|
+
*
|
|
4
|
+
* The stable read seam between the daemon and the SPA.
|
|
5
|
+
* FROZEN in WP0 — method names and return-type shapes must not change without
|
|
6
|
+
* a plan decision (changing them breaks WP2/WP3/WP4 concurrently in flight).
|
|
7
|
+
*
|
|
8
|
+
* Ownership split (for parallel fan-out per §12):
|
|
9
|
+
* - overview.ts: WP2 (getGlobalOverview, listWorkspaces, getWorkspace,
|
|
10
|
+
* listSessions, getSession, getTurnTimeline)
|
|
11
|
+
* - settings.ts: WP4 (getSettings, updateSettings)
|
|
12
|
+
*
|
|
13
|
+
* The UI never imports sql or better-sqlite3 directly. All data access goes
|
|
14
|
+
* through this barrel.
|
|
15
|
+
*/
|
|
16
|
+
export { stubResponse, buildResponse } from "../envelope.js";
|
|
17
|
+
export { getGlobalOverview, listWorkspaces, getWorkspace, listSessions, getSession, getTurnTimeline, listLiveSessions, } from "./overview.js";
|
|
18
|
+
export { getSettings, updateSettings, resetDatabase, calibrateLimit, calibrateBytesPerToken, } from "./settings.js";
|
|
19
|
+
export { listRecommendations, getRecommendationCard, dismissRecommendation, adoptRecommendation, deriveActiveGroups, buildSeededPrompt, } from "./recommendations.js";
|
|
20
|
+
export { startApplyJob, getApplyJob, confirmApplyJob, rollbackApplyJob } from "../../apply/jobs.js";
|
|
21
|
+
export { openTerminalForRec } from "../../apply/open-terminal.js";
|
|
22
|
+
export { listLedger } from "./recommendations-ledger.js";
|
|
23
|
+
export { getSuccessRate, listWorkspaceOutcomes, getWorkspaceOutcomeDetail, getLinkageRate, } from "./outcomes.js";
|
|
24
|
+
export { generateWeeklyReport, listReports, getReport } from "./reports.js";
|
|
25
|
+
export { classifyCacheEfficiency, getFlavorDecomposition, getCacheEfficiency, } from "./spend-flavor.js";
|
|
26
|
+
export { getCacheWriteTrend, detectSpikes } from "./trends.js";
|
|
27
|
+
export { getContextComposition } from "./context-composition.js";
|
|
28
|
+
export { getContextBudget } from "./context-budget.js";
|
|
29
|
+
export { getDeliveryMetrics } from "./delivery.js";
|
|
30
|
+
export { computeSessionDelivery, getAbandonedSpendSplit, getClosureProxy, getSessionClosureStatus, } from "./effectiveness.js";
|
|
31
|
+
export { getSessionDrivers } from "./session-drivers.js";
|
|
32
|
+
export { getLoopGuard } from "./loop-guard.js";
|
|
33
|
+
export { getHookConfig, updateHookConfig } from "./hook-config.js";
|
|
34
|
+
export { installHookRoute, uninstallHookRoute } from "./hook-install.js";
|
|
35
|
+
export { getIdleSessions } from "./idle-sessions.js";
|
|
36
|
+
export { getBurnStatus } from "./burn-status.js";
|
|
37
|
+
export { getSelfChurn } from "./self-churn.js";
|
|
38
|
+
export { getOffloadShare } from "./offload-share.js";
|
|
39
|
+
export { getSessionSpendPercentile, getWeeklySelfPercentile } from "./self-percentiles.js";
|
|
40
|
+
export { getCostPerSuccess } from "./cost-per-success.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Fail-open D7 repeated-failure circuit breaker for the local hook. */
|
|
2
|
+
import { getQueryDb } from "../db-context.js";
|
|
3
|
+
import { buildResponse } from "../envelope.js";
|
|
4
|
+
import { DEFAULT_HOOK_CONFIG, readHookConfig } from "./hook-config.js";
|
|
5
|
+
function response(data) {
|
|
6
|
+
return buildResponse(data, {
|
|
7
|
+
claim_kind: "OBS_PROXY",
|
|
8
|
+
n: data.failing_run_len,
|
|
9
|
+
drilldown_ids: { session_id: data.session_id },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function okGuard(sessionId, reason, failCountThreshold = DEFAULT_HOOK_CONFIG.d7_fail_count, windowTurns = DEFAULT_HOOK_CONFIG.d7_window_turns) {
|
|
13
|
+
return {
|
|
14
|
+
stage: "ok",
|
|
15
|
+
session_id: sessionId,
|
|
16
|
+
identical_run_len: 0,
|
|
17
|
+
failing_run_len: 0,
|
|
18
|
+
fail_count_threshold: failCountThreshold,
|
|
19
|
+
window_turns: windowTurns,
|
|
20
|
+
reason,
|
|
21
|
+
ts: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function isFailingExitClass(exitClass) {
|
|
25
|
+
return exitClass === "TEST_FAIL" || exitClass === "ERROR";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Return a bounded, strictly fail-open D7 guard result. The endpoint returns
|
|
29
|
+
* only measured counts and identifiers; tool input and transcript text remain
|
|
30
|
+
* outside this query and response.
|
|
31
|
+
*/
|
|
32
|
+
export function getLoopGuard(sessionId) {
|
|
33
|
+
try {
|
|
34
|
+
const db = getQueryDb();
|
|
35
|
+
const config = readHookConfig(db);
|
|
36
|
+
const events = db
|
|
37
|
+
.prepare(`SELECT event_id, ts, input_hash, exit_class
|
|
38
|
+
FROM tool_events INDEXED BY idx_tool_ts_session_event
|
|
39
|
+
WHERE session_id = ?
|
|
40
|
+
ORDER BY ts DESC, event_id DESC
|
|
41
|
+
LIMIT ?`)
|
|
42
|
+
.all(sessionId, config.d7_window_turns * 8);
|
|
43
|
+
if (events.length === 0) {
|
|
44
|
+
const session = db
|
|
45
|
+
.prepare("SELECT 1 AS present FROM sessions WHERE session_id = ? LIMIT 1")
|
|
46
|
+
.get(sessionId);
|
|
47
|
+
return response(okGuard(sessionId, session === undefined ? "unknown_session" : "no_events", config.d7_fail_count, config.d7_window_turns));
|
|
48
|
+
}
|
|
49
|
+
if (events.some((event) => !Number.isFinite(Date.parse(event.ts)))) {
|
|
50
|
+
return response(okGuard(sessionId, "invalid_timestamp", config.d7_fail_count, config.d7_window_turns));
|
|
51
|
+
}
|
|
52
|
+
const latest = events[0];
|
|
53
|
+
if (latest === undefined) {
|
|
54
|
+
return response(okGuard(sessionId, "no_events", config.d7_fail_count, config.d7_window_turns));
|
|
55
|
+
}
|
|
56
|
+
const inputHash = latest.input_hash;
|
|
57
|
+
const trailingRun = [];
|
|
58
|
+
if (inputHash !== null) {
|
|
59
|
+
for (const event of events) {
|
|
60
|
+
if (event.input_hash !== inputHash)
|
|
61
|
+
break;
|
|
62
|
+
trailingRun.push(event);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const identicalRunLen = trailingRun.length;
|
|
66
|
+
const failingRunLen = trailingRun.filter((event) => isFailingExitClass(event.exit_class)).length;
|
|
67
|
+
const stage = failingRunLen >= config.d7_fail_count
|
|
68
|
+
? "block"
|
|
69
|
+
: failingRunLen >= Math.max(2, config.d7_fail_count - 1)
|
|
70
|
+
? "warn"
|
|
71
|
+
: "ok";
|
|
72
|
+
return response({
|
|
73
|
+
stage,
|
|
74
|
+
session_id: sessionId,
|
|
75
|
+
identical_run_len: identicalRunLen,
|
|
76
|
+
failing_run_len: failingRunLen,
|
|
77
|
+
fail_count_threshold: config.d7_fail_count,
|
|
78
|
+
window_turns: config.d7_window_turns,
|
|
79
|
+
reason: stage === "block"
|
|
80
|
+
? "repeated_identical_failures"
|
|
81
|
+
: stage === "warn"
|
|
82
|
+
? "near_repeated_identical_failures"
|
|
83
|
+
: "below_threshold",
|
|
84
|
+
ts: latest.ts,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return response(okGuard(sessionId, "unavailable"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/query/api/offload-share.ts — query-only L2c subagent offload metrics.
|
|
3
|
+
*
|
|
4
|
+
* Offload share uses structural turn counts only; no transcript, tool text,
|
|
5
|
+
* or paths leave this surface (SEC-101).
|
|
6
|
+
*/
|
|
7
|
+
import { buildResponse } from "../envelope.js";
|
|
8
|
+
/**
|
|
9
|
+
* Derive subagent offload share for non-provisional turns in [from, to).
|
|
10
|
+
*/
|
|
11
|
+
export function getOffloadShare(db, opts) {
|
|
12
|
+
const workspaceFilter = opts.workspaceId === null ? "" : " AND workspace_id = ?";
|
|
13
|
+
const params = opts.workspaceId === null ? [opts.from, opts.to] : [opts.from, opts.to, opts.workspaceId];
|
|
14
|
+
const totals = db
|
|
15
|
+
.prepare(`SELECT COUNT(DISTINCT session_id) AS session_count,
|
|
16
|
+
COUNT(DISTINCT CASE WHEN is_sidechain = 1 THEN session_id END) AS sidechain_session_count,
|
|
17
|
+
COUNT(*) AS total_turns,
|
|
18
|
+
COALESCE(SUM(CASE WHEN is_sidechain = 1 THEN 1 ELSE 0 END), 0) AS sidechain_turns
|
|
19
|
+
FROM turns
|
|
20
|
+
WHERE ts >= ? AND ts < ? AND provisional = 0${workspaceFilter}`)
|
|
21
|
+
.get(...params);
|
|
22
|
+
const data = {
|
|
23
|
+
...totals,
|
|
24
|
+
offload_share: totals.total_turns === 0 ? null : totals.sidechain_turns / totals.total_turns,
|
|
25
|
+
from: opts.from,
|
|
26
|
+
to: opts.to,
|
|
27
|
+
workspace_id: opts.workspaceId,
|
|
28
|
+
};
|
|
29
|
+
return buildResponse(data, {
|
|
30
|
+
claim_kind: "OBS_PROXY",
|
|
31
|
+
n: data.total_turns,
|
|
32
|
+
window: { from: opts.from, to: opts.to },
|
|
33
|
+
qualification: {
|
|
34
|
+
provisional_excluded: true,
|
|
35
|
+
unpriced_turns: 0,
|
|
36
|
+
claim_kinds_count: 1,
|
|
37
|
+
note: "Within-session subagent offload share: is_sidechain turns / all turns (structural).",
|
|
38
|
+
},
|
|
39
|
+
...(opts.workspaceId === null ? {} : { drilldown_ids: { workspace_id: opts.workspaceId } }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/query/api/outcomes.ts — LocalQueryAPI: outcomes surface.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors settings.ts pattern: getQueryDb() + buildResponse + typed payload.
|
|
5
|
+
*
|
|
6
|
+
* All responses carry claim_kind:'EXPERIMENTAL' + the 73% methodology
|
|
7
|
+
* disclosure (plan §3 + §6 Q2). COND-1 gate applied in denominators.
|
|
8
|
+
*
|
|
9
|
+
* 73% note: fixed "73% (validation corpus)" methodology disclosure shown
|
|
10
|
+
* ALONGSIDE the live per-workspace getLinkageRate number — never conflated.
|
|
11
|
+
*
|
|
12
|
+
* SQL references: Data Model v2 §2 lines 261-275 / 303-314 / 329-334.
|
|
13
|
+
*/
|
|
14
|
+
import { getQueryDb } from "../db-context.js";
|
|
15
|
+
import { buildResponse } from "../envelope.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Methodology disclosure (fixed — plan §6 Q2)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const LINKAGE_METHODOLOGY_NOTE = "73% (validation corpus): linkage methodology validated on a sample corpus. " +
|
|
20
|
+
"Live linkage rate is per-workspace and shown separately in getLinkageRate. " +
|
|
21
|
+
"All findings are EXPERIMENTAL and excluded from gated denominators (COND-1).";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helper: experimental envelope meta
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function experimentalMeta(n, from) {
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
return {
|
|
28
|
+
claim_kind: "EXPERIMENTAL",
|
|
29
|
+
n,
|
|
30
|
+
window: { from: from ?? "1970-01-01T00:00:00Z", to: now },
|
|
31
|
+
qualification: {
|
|
32
|
+
provisional_excluded: true,
|
|
33
|
+
unpriced_turns: 0,
|
|
34
|
+
claim_kinds_count: 1,
|
|
35
|
+
note: LINKAGE_METHODOLOGY_NOTE,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// getSuccessRate
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Return the global observed success rate.
|
|
44
|
+
* Denominator: terminal (non-IN_PROGRESS) work_items with ≥1 session_work_links.
|
|
45
|
+
* COND-1: findings from EXPERIMENTAL extractors excluded from deferral check.
|
|
46
|
+
*/
|
|
47
|
+
export function getSuccessRate() {
|
|
48
|
+
const db = getQueryDb();
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
const windowStart = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
51
|
+
// SQL from Data Model v2 §2 lines 261-275
|
|
52
|
+
const row = db
|
|
53
|
+
.prepare(`SELECT
|
|
54
|
+
COUNT(*) AS terminal_n,
|
|
55
|
+
AVG(outcome IN ('OBSERVED_SUCCESS','OBSERVED_SUCCESS_WITH_DEFERRALS')) AS success_rate,
|
|
56
|
+
SUM(outcome = 'OBSERVED_SUCCESS') AS clean_success_n,
|
|
57
|
+
SUM(outcome = 'OBSERVED_SUCCESS_WITH_DEFERRALS') AS with_deferrals_n,
|
|
58
|
+
SUM(outcome IN ('OBSERVED_SUCCESS','OBSERVED_SUCCESS_WITH_DEFERRALS')
|
|
59
|
+
AND wi.checks_conclusion = 'NONE') AS no_ci_success_n
|
|
60
|
+
FROM observed_outcomes o JOIN work_items wi USING (work_item_id)
|
|
61
|
+
WHERE o.outcome != 'IN_PROGRESS'
|
|
62
|
+
AND COALESCE(wi.merged_at, wi.closed_at) >= ?
|
|
63
|
+
AND COALESCE(wi.merged_at, wi.closed_at) < ?
|
|
64
|
+
AND EXISTS (SELECT 1 FROM session_work_links l WHERE l.work_item_id = o.work_item_id)`)
|
|
65
|
+
.get(windowStart, now);
|
|
66
|
+
// SQL from Data Model v2 §2 lines 329-334
|
|
67
|
+
const lrRow = db
|
|
68
|
+
.prepare(`SELECT AVG(EXISTS(SELECT 1 FROM session_work_links l WHERE l.session_id = s.session_id))
|
|
69
|
+
AS linkage_rate
|
|
70
|
+
FROM sessions s WHERE s.state='RECONCILED'
|
|
71
|
+
AND EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id
|
|
72
|
+
AND te.tool_name = 'Bash')`)
|
|
73
|
+
.get();
|
|
74
|
+
if (row === undefined || row.terminal_n === 0) {
|
|
75
|
+
const data = {
|
|
76
|
+
terminal_n: 0,
|
|
77
|
+
success_rate: null,
|
|
78
|
+
clean_success_n: 0,
|
|
79
|
+
with_deferrals_n: 0,
|
|
80
|
+
no_ci_success_n: 0,
|
|
81
|
+
linkage_rate: lrRow?.linkage_rate ?? null,
|
|
82
|
+
methodology_note: LINKAGE_METHODOLOGY_NOTE,
|
|
83
|
+
};
|
|
84
|
+
return buildResponse(data, experimentalMeta(0, windowStart));
|
|
85
|
+
}
|
|
86
|
+
const data = {
|
|
87
|
+
terminal_n: row.terminal_n,
|
|
88
|
+
success_rate: row.success_rate,
|
|
89
|
+
clean_success_n: row.clean_success_n,
|
|
90
|
+
with_deferrals_n: row.with_deferrals_n,
|
|
91
|
+
no_ci_success_n: row.no_ci_success_n,
|
|
92
|
+
linkage_rate: lrRow?.linkage_rate ?? null,
|
|
93
|
+
methodology_note: LINKAGE_METHODOLOGY_NOTE,
|
|
94
|
+
};
|
|
95
|
+
return buildResponse(data, experimentalMeta(row.terminal_n, windowStart));
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// listWorkspaceOutcomes
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Return per-workspace outcome summaries.
|
|
102
|
+
*/
|
|
103
|
+
export function listWorkspaceOutcomes() {
|
|
104
|
+
const db = getQueryDb();
|
|
105
|
+
const rows = db
|
|
106
|
+
.prepare(`WITH linked_items AS (
|
|
107
|
+
SELECT wi.*, o.outcome
|
|
108
|
+
FROM work_items wi
|
|
109
|
+
LEFT JOIN observed_outcomes o ON o.work_item_id = wi.work_item_id
|
|
110
|
+
WHERE EXISTS (
|
|
111
|
+
SELECT 1 FROM session_work_links l WHERE l.work_item_id = wi.work_item_id
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
SELECT w.workspace_id, w.project_slug,
|
|
115
|
+
COUNT(li.work_item_id) AS total_n,
|
|
116
|
+
SUM(CASE WHEN li.state = 'OPEN' THEN 1 ELSE 0 END) AS in_progress_n,
|
|
117
|
+
SUM(CASE WHEN li.state IN ('MERGED', 'CLOSED') THEN 1 ELSE 0 END) AS terminal_n,
|
|
118
|
+
SUM(CASE WHEN li.outcome IN ('OBSERVED_SUCCESS','OBSERVED_SUCCESS_WITH_DEFERRALS')
|
|
119
|
+
THEN 1 ELSE 0 END) AS success_n,
|
|
120
|
+
SUM(CASE WHEN li.outcome = 'OBSERVED_FAILURE' THEN 1 ELSE 0 END) AS failure_n
|
|
121
|
+
FROM workspaces w
|
|
122
|
+
LEFT JOIN linked_items li ON li.workspace_id = w.workspace_id
|
|
123
|
+
GROUP BY w.workspace_id, w.project_slug
|
|
124
|
+
ORDER BY w.project_slug, w.workspace_id`)
|
|
125
|
+
.all();
|
|
126
|
+
// Compute per-workspace linkage rates
|
|
127
|
+
const lrRows = db
|
|
128
|
+
.prepare(`SELECT s.workspace_id,
|
|
129
|
+
AVG(EXISTS(SELECT 1 FROM session_work_links l WHERE l.session_id = s.session_id))
|
|
130
|
+
AS linkage_rate
|
|
131
|
+
FROM sessions s WHERE s.state='RECONCILED'
|
|
132
|
+
AND EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id
|
|
133
|
+
AND te.tool_name = 'Bash')
|
|
134
|
+
GROUP BY s.workspace_id`)
|
|
135
|
+
.all();
|
|
136
|
+
const lrByWs = new Map(lrRows.map((r) => [r.workspace_id, r.linkage_rate]));
|
|
137
|
+
const adherenceRows = db
|
|
138
|
+
.prepare(`SELECT workspace_id, COUNT(*) AS n,
|
|
139
|
+
AVG(CASE WHEN model LIKE '%opus%' THEN 1.0 ELSE 0.0 END) AS premium_share
|
|
140
|
+
FROM turns
|
|
141
|
+
WHERE is_sidechain = 0 AND provisional = 0
|
|
142
|
+
GROUP BY workspace_id`)
|
|
143
|
+
.all();
|
|
144
|
+
const adherenceByWs = new Map(adherenceRows.map((r) => [
|
|
145
|
+
r.workspace_id,
|
|
146
|
+
r.n > 0 && r.premium_share !== null ? Math.round(100 * (1 - r.premium_share)) : null,
|
|
147
|
+
]));
|
|
148
|
+
const data = rows.map((r) => ({
|
|
149
|
+
...r,
|
|
150
|
+
success_rate: r.terminal_n > 0 ? r.success_n / r.terminal_n : null,
|
|
151
|
+
linkage_rate: lrByWs.get(r.workspace_id) ?? null,
|
|
152
|
+
adherence_score: adherenceByWs.get(r.workspace_id) ?? null,
|
|
153
|
+
}));
|
|
154
|
+
return buildResponse(data, experimentalMeta(data.length));
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// getWorkspaceOutcomeDetail
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Return detail for a single work item (findings + linked sessions).
|
|
161
|
+
*/
|
|
162
|
+
export function getWorkspaceOutcomeDetail(workItemId) {
|
|
163
|
+
const db = getQueryDb();
|
|
164
|
+
const wi = db
|
|
165
|
+
.prepare(`SELECT wi.work_item_id, wi.number, wi.state, wi.checks_conclusion,
|
|
166
|
+
wi.merged_at, wi.closed_at, o.outcome
|
|
167
|
+
FROM work_items wi
|
|
168
|
+
LEFT JOIN observed_outcomes o USING (work_item_id)
|
|
169
|
+
WHERE wi.work_item_id = ?`)
|
|
170
|
+
.get(workItemId);
|
|
171
|
+
if (wi === undefined) {
|
|
172
|
+
return buildResponse(null, experimentalMeta(0));
|
|
173
|
+
}
|
|
174
|
+
const findings = db
|
|
175
|
+
.prepare(`SELECT finding_id, source, severity, status, evidence_ref,
|
|
176
|
+
raised_at, cleared_at, extractor_version
|
|
177
|
+
FROM review_findings WHERE work_item_id = ? ORDER BY raised_at`)
|
|
178
|
+
.all(workItemId);
|
|
179
|
+
const linkedSessions = db
|
|
180
|
+
.prepare("SELECT session_id FROM session_work_links WHERE work_item_id = ?")
|
|
181
|
+
.all(workItemId).map((r) => r.session_id);
|
|
182
|
+
const data = {
|
|
183
|
+
...wi,
|
|
184
|
+
findings,
|
|
185
|
+
linked_sessions: linkedSessions,
|
|
186
|
+
};
|
|
187
|
+
return buildResponse(data, experimentalMeta(1));
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// getLinkageRate
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
/**
|
|
193
|
+
* Return the live linkage rate (per §6 Q2: separate from the 73% disclosure).
|
|
194
|
+
* SQL from Data Model v2 §2 lines 329-334.
|
|
195
|
+
*/
|
|
196
|
+
export function getLinkageRate(workspaceId) {
|
|
197
|
+
const db = getQueryDb();
|
|
198
|
+
const whereExtra = workspaceId !== undefined ? " AND s.workspace_id = ?" : "";
|
|
199
|
+
const params = workspaceId !== undefined ? [workspaceId] : [];
|
|
200
|
+
const row = db
|
|
201
|
+
.prepare(`SELECT AVG(EXISTS(SELECT 1 FROM session_work_links l WHERE l.session_id = s.session_id))
|
|
202
|
+
AS linkage_rate,
|
|
203
|
+
COUNT(*) AS denominator_n
|
|
204
|
+
FROM sessions s WHERE s.state='RECONCILED'
|
|
205
|
+
AND EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id
|
|
206
|
+
AND te.tool_name = 'Bash')${whereExtra}`)
|
|
207
|
+
.get(...params);
|
|
208
|
+
const data = {
|
|
209
|
+
linkage_rate: row?.linkage_rate ?? null,
|
|
210
|
+
denominator_n: row?.denominator_n ?? 0,
|
|
211
|
+
methodology_note: "Live per-workspace linkage rate. Denominator: RECONCILED sessions with ≥1 Bash tool_event. " +
|
|
212
|
+
"See also the fixed 73% validation corpus disclosure in getSuccessRate.",
|
|
213
|
+
};
|
|
214
|
+
return buildResponse(data, {
|
|
215
|
+
...experimentalMeta(data.denominator_n),
|
|
216
|
+
...(workspaceId !== undefined ? { drilldown_ids: { workspace_id: workspaceId } } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|