Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var { forkDaemon, devMode, setDaemonWatcherOpts } = require("../lib/cli/daemon-l
var { setup, promptRestoreProjects, showMainMenu } = require("../lib/cli/menus");
var { getLocalIP } = require("../lib/cli/net-detect");
var { log, a, sym } = require("../lib/cli/tui");
var { handleShutdown, handleRestart, handleAdd, handleRemove, handleList } = require("../lib/cli/ipc-subcommands");
var { handleShutdown, handleRestart, handleAdd, handleRemove, handleList, handleActivityDiagnostics } = require("../lib/cli/ipc-subcommands");

var args = process.argv.slice(2);

Expand Down Expand Up @@ -69,6 +69,7 @@ var noRestart = false;
var addPath = null;
var removePath = null;
var listMode = false;
var activityDiagnosticsMode = false;
var dangerouslySkipPermissions = false;
var headlessMode = false;
var watchMode = false;
Expand Down Expand Up @@ -118,6 +119,8 @@ for (var i = 0; i < args.length; i++) {
i++;
} else if (args[i] === "--list") {
listMode = true;
} else if (args[i] === "--activity-diagnostics") {
activityDiagnosticsMode = true;
} else if (args[i] === "--headless") {
headlessMode = true;
autoYes = true;
Expand All @@ -132,6 +135,7 @@ for (var i = 0; i < args.length; i++) {
console.log(" clagentic-console --add <path> Add a project to the running daemon");
console.log(" clagentic-console --remove <path> Remove a project from the running daemon");
console.log(" clagentic-console --list List registered projects");
console.log(" clagentic-console --activity-diagnostics Print activity-divergence probe totals as JSON (agent-readable)");
console.log(" clagentic-console release list-betas List promotable beta versions (maintainer/release-engineering)");
console.log("");
console.log("Options:");
Expand All @@ -149,6 +153,7 @@ for (var i = 0; i < args.length; i++) {
console.log(" --add <path> Add a project directory (use '.' for current)");
console.log(" --remove <path> Remove a project directory");
console.log(" --list List all registered projects");
console.log(" --activity-diagnostics Print activity-divergence probe totals as JSON");
console.log(" --headless Start daemon and exit immediately (implies --yes)");
console.log(" --multi-user Start in multi-user mode (use with --yes for headless)");
console.log(" --os-users Enable OS-level user isolation (Linux, requires root + --multi-user)");
Expand Down Expand Up @@ -194,6 +199,12 @@ if (listMode) {
return;
}

// --- Handle --activity-diagnostics before anything else ---
if (activityDiagnosticsMode) {
handleActivityDiagnostics();
return;
}

// --multi-user / --os-users are now handled in the main entry flow (setup wizard or repeat run)
// Flags are parsed above and applied during forkDaemon()

Expand Down
45 changes: 44 additions & 1 deletion docs/guides/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,50 @@ graph TB
P2 --- Sessions2
```

The daemon is spawned with `detached: true` and survives CLI exit. Multiple CLI instances share one daemon. IPC commands include `add_project`, `remove_project`, `set_pin`, `set_keep_awake`, `shutdown`, `get_status`.
The daemon is spawned with `detached: true` and survives CLI exit. Multiple CLI instances share one daemon. IPC commands include `add_project`, `remove_project`, `set_pin`, `set_keep_awake`, `shutdown`, `get_status`, `get_activity_diagnostics`.

### Agent-readable diagnostics over IPC

Some server-side diagnostic probes (e.g. the activity-source divergence
counter that sizes the `session.isProcessing` vs. registry-derived-active
redesign) are recorded server-side but were historically only reachable
through a live WebSocket client (`process_stats`) — unreachable by a
read-only crew agent holding only `Bash`+`Read`, no browser, no WS client,
no devtools.

The daemon.sock IPC channel above is already reachable by such an agent (it
is a plain Unix socket, no browser or WS client required), and is gated by
filesystem permissions rather than any per-command auth check: `CONFIG_DIR`
(containing `daemon.sock`) is created `chmod 0700` (`ensureConfigDir`, `lib/config.js`),
so only the daemon's own OS user can connect at all. A read-only diagnostics
command added to this channel inherits that gate for free — it does not
copy the auth gap `process_stats`'s WS handler has (`lib/project-sessions.js:668-709`,
tracked separately), since it is a different transport with its own,
stricter gate.

**The exact command a Bash-only agent runs** to read the activity-divergence
probe from a running daemon:

```
clagentic-console --activity-diagnostics
```

This prints JSON to stdout:

```json
{
"activeLiveCount": 0,
"activityDivergenceCount": 0,
"activityDivergenceRecentSamples": []
}
```

Samples carry no session-identifying field (`ts`, `rawIsProcessing`,
`derivedIsActive`, `hasQueryInstance` only) — the same shape
`process_stats` already carries, unchanged by adding this second retrieval
path. See `lib/sdk-bridge.js`'s `buildActivityDiagnosticsResponse()` (the
shared, unit-tested function both retrieval paths can call) and
`lib/daemon.js`'s `get_activity_diagnostics` IPC case.

## YOKE Adapter Layer

Expand Down
44 changes: 40 additions & 4 deletions lib/cli/ipc-subcommands.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// lib/cli/ipc-subcommands.js
//
// One-shot IPC subcommand handlers for bin/cli.js: --shutdown, --restart,
// --add <path>, --remove <path>, --list. Each of these talks to the running
// daemon over the Unix socket and exits the process directly (they never
// return control to the caller) — extracted verbatim from bin/cli.js
// (lr-4e49 Part 1), no behavior change.
// --add <path>, --remove <path>, --list, --activity-diagnostics. Each of
// these talks to the running daemon over the Unix socket and exits the
// process directly (they never return control to the caller) —
// --shutdown/--restart/--add/--remove/--list extracted verbatim from
// bin/cli.js (lr-4e49 Part 1), no behavior change.
// --activity-diagnostics added by lr-8b476f: the only agent-readable
// (Bash+Read, no browser/WS/devtools) retrieval path for the lr-58c813
// server-side activity-divergence probe. Prints raw JSON to stdout so a
// read-only crew agent can run this directly.

var fs = require("fs");
var path = require("path");
Expand Down Expand Up @@ -177,10 +182,41 @@ function handleList() {
});
}

// lr-8b476f: prints the activity-divergence probe totals (same data
// process_stats's WS response folds in, see lib/daemon.js's
// "get_activity_diagnostics" IPC case) as raw JSON to stdout. This is the
// retrieval path for a read-only crew agent (Bash+Read only, no browser,
// no WS client, no devtools): `clagentic-console --activity-diagnostics`.
function handleActivityDiagnostics() {
var diagConfig = loadConfig();
isDaemonAliveAsync(diagConfig).then(function (alive) {
if (!alive) {
console.error("No running daemon. Start with: npx @clagentic/console");
process.exit(1);
}
sendIPCCommand(socketPath(), { cmd: "get_activity_diagnostics" }).then(function (res) {
if (!res.ok) {
console.error("Failed: " + (res.error || "unknown error"));
process.exit(1);
return;
}
// Raw JSON to stdout — the point is machine readability, not a
// human-formatted summary (contrast handleList's formatted output).
console.log(JSON.stringify({
activeLiveCount: res.activeLiveCount,
activityDivergenceCount: res.activityDivergenceCount,
activityDivergenceRecentSamples: res.activityDivergenceRecentSamples,
}, null, 2));
process.exit(0);
});
});
}

module.exports = {
handleShutdown: handleShutdown,
handleRestart: handleRestart,
handleAdd: handleAdd,
handleRemove: handleRemove,
handleList: handleList,
handleActivityDiagnostics: handleActivityDiagnostics,
};
26 changes: 25 additions & 1 deletion lib/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var usersModule = require("./users");
var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
var { validateCloneUrl, buildCloneArgs } = require("./clone-validate");
var { DEFAULT_MEM_AVAILABLE_MIN_MB, DEFAULT_TOKENS_PER_MB_HEADROOM, getActiveLiveCount } = require("./sdk-bridge");
var { DEFAULT_MEM_AVAILABLE_MIN_MB, DEFAULT_TOKENS_PER_MB_HEADROOM, getActiveLiveCount, buildActivityDiagnosticsResponse } = require("./sdk-bridge");
var { startMemoryHighWatcher, checkAppliedMemoryCeiling } = require("./memory-limits");
var { createDrain } = require("./drain");
var { shedMemory } = require("./memory-shed");
Expand Down Expand Up @@ -1553,6 +1553,30 @@ var ipc = createIPCServer(socketPath(), function (msg) {
uptime: process.uptime(),
};

// lr-8b476f: agent-readable counterpart to the WS-only process_stats
// handler (lib/project-sessions.js:668-709). Reads the same shared
// module-level counter (lib/sdk-bridge.js) that process_stats already
// folds into its response via getMemoryStats() — buildActivityDiagnosticsResponse
// is unit-tested directly (test/activity-diagnostics-retrieval-lr-8b476f.test.js),
// since daemon.js has no module.exports and cannot be required
// in-process without binding real sockets/HTTP servers.
//
// Read-only, mutates nothing. Gated the same way every other IPC
// command on this socket is gated: CONFIG_DIR (containing daemon.sock)
// is chmod 0700 (ensureConfigDir, lib/config.js), so only the daemon's
// own OS user can connect at all — no new auth surface is introduced,
// and none is copied from process_stats's WS gap (lr-2016fe) either,
// since this is a different transport with its own (stricter, OS-level)
// gate rather than an unaudited role check.
//
// Samples carry no session-identifying field, matching process_stats's
// BOBBIE-remediated shape exactly (lib/sdk-bridge.js
// _recordActivityDivergenceIfAny) — this command reads the SAME
// samples, so nothing new is exposed by adding this second retrieval
// path.
case "get_activity_diagnostics":
return buildActivityDiagnosticsResponse();

case "set_pin": {
config.pinHash = msg.pinHash || null;
relay.setAuthToken(config.pinHash);
Expand Down
20 changes: 19 additions & 1 deletion lib/sdk-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ function getActivityDivergenceStats() {
};
}

// lr-8b476f: shared response builder for the agent-readable retrieval path
// (daemon.js's "get_activity_diagnostics" IPC command, over the existing
// daemon.sock Unix socket). Pulled out here — rather than inlined in
// daemon.js, which has no module.exports and cannot be safely required by a
// test without binding real sockets/HTTP servers — so this is unit-testable
// the same way getActivityDivergenceStats()/getActiveLiveCount() already
// are. Read-only: calls only the two existing module-level accessors above,
// writes nothing.
function buildActivityDiagnosticsResponse() {
var divergence = getActivityDivergenceStats();
return {
ok: true,
activeLiveCount: getActiveLiveCount(),
activityDivergenceCount: divergence.count,
activityDivergenceRecentSamples: divergence.recentSamples,
};
}

// --- lr-2d91: MemAvailable gate ---
// Default minimum available memory threshold in MB. Referenced by sdk-bridge
// and daemon.js — changing this constant is the single place to adjust the default.
Expand Down Expand Up @@ -2490,5 +2508,5 @@ function getActiveLiveCount() {
return _activeLiveCount;
}

module.exports = { createSDKBridge, createMessageQueue, readMemAvailableMB, readCgroupHeadroomMB, DEFAULT_MEM_AVAILABLE_MIN_MB, DEFAULT_TOKENS_PER_MB_HEADROOM, getActiveLiveCount, getActivityDivergenceStats };
module.exports = { createSDKBridge, createMessageQueue, readMemAvailableMB, readCgroupHeadroomMB, DEFAULT_MEM_AVAILABLE_MIN_MB, DEFAULT_TOKENS_PER_MB_HEADROOM, getActiveLiveCount, getActivityDivergenceStats, buildActivityDiagnosticsResponse };

Loading
Loading