feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from Settings → Dashboard to the Claude Code tab's AgentHub sub-tab (ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub. A home that is or contains the OS home is refused (EBADHOME). Settings persistence: dashboard-saved settings go to ~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin cache an update wipes. Load order: real env > the checkout's .env > that file. The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put. C1: codex-usage.js reads Codex rollout journals into sessions (new column tool) and token_usage, tracked per file in codex_rollouts; forks deduped by total vector, sub-agent threads folded into the root; pricing-sync also prices OpenAI models; PricingSettings with a vendor filter. C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on a click, through a stable forwarder; Codex trust is never written. The hook route creates tool='codex' sessions and folds sub-agent hooks into the root. D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab; Gemini pricing. Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=, clear-data covering the new tables, Codex child-thread events moving no status, hooks owning status only while current, EFOREIGN for a forwarder that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE honoured from .env. Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
This commit is contained in:
+44
-10
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* @file Codex lane status mirror. Codex emits no hooks, so nothing else can
|
||||
* tell the dashboard that a `codex` lane's agent is working. Its on-disk
|
||||
* signal is the rollout journal it appends live, one file per thread:
|
||||
* @file Codex lane status mirror. Without AgentHub's Codex hooks installed
|
||||
* (codex-hooks.js, opt-in and trusted by the user in Codex) nothing else tells
|
||||
* the dashboard that a `codex` lane's agent is working; while a lane's hooks
|
||||
* are current (an event for its session in the last 2 minutes) this mirror
|
||||
* leaves the lane to them. Its on-disk signal is the rollout journal Codex
|
||||
* appends live, one file per thread:
|
||||
*
|
||||
* $CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<threadId>.jsonl
|
||||
* line 1: {"type":"session_meta","payload":{"cwd":…,"session_id":…}}
|
||||
@@ -15,8 +18,9 @@
|
||||
* Codex 0.149 also mirrors threads into version-suffixed SQLite files
|
||||
* (`state_5.sqlite`, `thread_history_1.sqlite`), but every thread row still
|
||||
* points at a live rollout file, so reading the JSONL costs no coupling to a
|
||||
* schema the next Codex release renames. `rolloutFiles()` is the one function
|
||||
* to re-point if that ever stops being true.
|
||||
* schema the next Codex release renames. `allRolloutFiles()` is the one
|
||||
* function to re-point if that ever stops being true (codex-usage.js walks
|
||||
* the same files for sessions and token usage).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -25,12 +29,13 @@ const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
const { getCodexHome } = require("./codex-home");
|
||||
|
||||
const FUTURE_SLACK_MS = 1000;
|
||||
const CODEX_ACTIVE_MS = 120_000; // a journal written within 2 min means the agent is working
|
||||
const MAX_FILES = 40; // newest-first cap so a years-deep sessions/ tree can't stall a pass
|
||||
const MAX_DEPTH = 4; // sessions/<YYYY>/<MM>/<DD>/<file>
|
||||
|
||||
/** Every rollout journal under sessions/, newest mtime first, capped at MAX_FILES. */
|
||||
function rolloutFiles() {
|
||||
/** Every rollout journal under sessions/ ({path, mtime, size}), unsorted. */
|
||||
function allRolloutFiles() {
|
||||
const out = [];
|
||||
const walk = (dir, depth) => {
|
||||
let entries = [];
|
||||
@@ -54,7 +59,14 @@ function rolloutFiles() {
|
||||
}
|
||||
};
|
||||
walk(path.join(getCodexHome(), "sessions"), 0);
|
||||
return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The newest MAX_FILES rollout journals, newest mtime first. */
|
||||
function rolloutFiles() {
|
||||
return allRolloutFiles()
|
||||
.sort((a, b) => b.mtime - a.mtime)
|
||||
.slice(0, MAX_FILES);
|
||||
}
|
||||
|
||||
const cwdCache = new Map(); // rollout path → thread cwd (written once, at thread creation)
|
||||
@@ -114,6 +126,10 @@ function ingestCodexOnce({ broadcast } = {}) {
|
||||
if (!codexLanes.length) return 0;
|
||||
|
||||
const touched = new Set();
|
||||
// One verdict per lane — running when ANY of its journals is fresh. Judging
|
||||
// file by file against the same status snapshot flipped a lane with an old
|
||||
// journal beside the live one between running and idle on every pass.
|
||||
const activeByLane = new Map();
|
||||
const files = rolloutFiles();
|
||||
pruneTracking(new Set(files.map((f) => f.path)));
|
||||
for (const file of files) {
|
||||
@@ -127,10 +143,26 @@ function ingestCodexOnce({ broadcast } = {}) {
|
||||
// An mtime in the FUTURE (clock stepped back, NTP correction, a restore that
|
||||
// preserved timestamps) makes the raw difference negative, which reads as
|
||||
// "written milliseconds ago". A file that claims to be from the future is
|
||||
// not evidence of an agent working now, so treat it as idle.
|
||||
// not evidence of an agent working now, so treat it as idle. The second of
|
||||
// slack is for the file's nanosecond mtime against Date.now()'s truncated
|
||||
// millisecond: a journal written this very millisecond reads as future.
|
||||
const age = Date.now() - file.mtime;
|
||||
const active = age >= 0 && age < CODEX_ACTIVE_MS;
|
||||
const active = age > -FUTURE_SLACK_MS && age < CODEX_ACTIVE_MS;
|
||||
const seen = activeByLane.get(lane.id);
|
||||
activeByLane.set(lane.id, { lane, active: active || (seen ? seen.active : false) });
|
||||
}
|
||||
|
||||
// Codex's hooks (codex-hooks.js) report a turn's start and end as they
|
||||
// happen; while they are current, a journal written a minute ago would call
|
||||
// a lane that just stopped "running".
|
||||
const { db } = require("../db");
|
||||
const hookedSince = new Date(Date.now() - CODEX_ACTIVE_MS).toISOString();
|
||||
const recentHook = db.prepare(
|
||||
"SELECT 1 FROM events WHERE session_id = ? AND created_at > ? LIMIT 1"
|
||||
);
|
||||
|
||||
for (const { lane, active } of activeByLane.values()) {
|
||||
if (lane.session_id && recentHook.get(lane.session_id, hookedSince)) continue;
|
||||
// Status mirror, under the same guard hook ingestion uses: only idle↔running,
|
||||
// and only for a lane the dashboard did not launch — provisioning and failed
|
||||
// are lifecycle states this path must never stomp on. `updateLane` filters
|
||||
@@ -181,7 +213,9 @@ function startCodexPoll(broadcast) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
allRolloutFiles,
|
||||
rolloutCwd,
|
||||
FUTURE_SLACK_MS,
|
||||
ingestCodexOnce,
|
||||
startCodexPoll,
|
||||
__resetCache,
|
||||
|
||||
Reference in New Issue
Block a user