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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.38.0] - 2026-06-14

Oracle activity reporting — the Oracle can now produce a cross-project "what happened
today" digest, exposed as a discovery tool (MCP + REST + OpenAPI), a dedicated endpoint,
and a web-UI tab.

### Added

- **`ActivityReporter`** (`oracle/services/activity_reporter.py`) — aggregates per-project
sessions, tool calls, edits, git mutations, and token/cost for a day (or `since`/`until`
window) across all registered projects, or one via `project_path`. Reads `.c3` JSONL
artifacts directly (no C3Runtime build); skips non-C3 projects without side effects.
- **`activity_report` discovery tool** (read tier) in `TOOL_SPECS` — auto-exposed on MCP,
OpenAPI, `POST /api/discovery/call`, and the internal Oracle chat. Optional `narrate=true`
adds a best-effort LLM prose summary (never fails the structured result).
- **`GET /api/activity/digest`** Oracle endpoint (`date` / `since` / `until` / `project` /
`narrate` query params) and an **Activity** tab in `oracle.html` with a date picker,
totals cards, optional narrative, and a per-project breakdown table.

### Changed

- `ChatEngine` accepts an optional `activity_reporter` and dispatches `activity_report`.

## [2.37.0] - 2026-06-14

Non-destructive config generation — regenerating instruction docs and applying permission
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
default. Generate, rotate, and copy the token from the dashboard's **Settings →
Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).

As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.

---

## Tiered local AI (optional)
Expand Down
2 changes: 1 addition & 1 deletion cli/c3.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
# Config
CONFIG_DIR = ".c3"
CONFIG_FILE = ".c3/config.json"
__version__ = "2.37.0"
__version__ = "2.38.0"


def _command_deps() -> CommandDeps:
Expand Down
1 change: 1 addition & 0 deletions cli/guide/oracle.html
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ <h3>Read tier — discovery</h3>
<tr><td class="grp-read"><code>c3_status</code></td> <td>Project health / budget / sessions overview</td></tr>
<tr><td class="grp-read"><code>c3_memory_query</code></td> <td>Read-only memory query (recall/list/score/graph/trends)</td></tr>
<tr><td class="grp-read"><code>c3_edits</code> / <code>c3_edits_cross</code></td><td>Query the edit ledger (one / all projects)</td></tr>
<tr><td class="grp-read"><code>activity_report</code></td><td>Cross-project daily digest: sessions, tool calls, edits, git mutations, token/cost (optional LLM narration)</td></tr>
</tbody>
</table>

Expand Down
34 changes: 34 additions & 0 deletions oracle-guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,40 @@ Dismiss an insight (marks `dismissed: true`, excluded from future listings).

---

## Activity

### `GET /api/activity/digest`

Cross-project activity digest for a day (or custom window). Aggregates sessions, tool
calls, edits, git mutations, and token/cost across all registered projects (or one, via
`project`).

**Query params:** `date` (UTC `YYYY-MM-DD`, default today); `since` / `until` (ISO bounds,
override `date`); `project` (single-project path); `narrate` (`true` adds an LLM prose
summary — best-effort, omitted on failure).

```json
{
"window": { "since": "2026-06-14T00:00:00", "until": "2026-06-14T23:59:59.999999",
"label": "2026-06-14 (today), UTC", "tz": "UTC" },
"totals": { "projects_active": 2, "sessions": 3, "tool_calls": 184, "edits": 57,
"git_mutations": 12, "decisions": 4,
"input_tokens": 120000, "output_tokens": 38000, "cost_usd": 1.42 },
"projects": [
{ "name": "claude-companion", "path": "/path/to/proj", "sessions": [ ... ],
"tool_calls": 150, "edits": 57, "git_mutations": 12,
"tokens": { "input": 100000, "output": 30000 }, "cost_usd": 1.10,
"first_activity": "2026-06-14T09:00:00+00:00", "last_activity": "2026-06-14T18:30:00+00:00" }
],
"narrative": null
}
```

> Also available as the discovery tool **`activity_report`** (identical payload) for
> external LLMs over MCP, OpenAPI, and `POST /api/discovery/call`.

---

## Suggestions

Suggestions are write-back recommendations that Oracle generates. They modify project `.c3/facts/facts.json` only when explicitly approved by the user.
Expand Down
14 changes: 14 additions & 0 deletions oracle-guide/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Oracle Changelog

## v1.2.0 (2026-06-14)

### Activity Reporting (C3 v2.38.0)
- **`ActivityReporter`** (`oracle/services/activity_reporter.py`): cross-project daily
digest aggregating sessions, tool calls, edits, git mutations, and token/cost. Reads
`.c3` JSONL artifacts directly per project (no C3Runtime build); skips non-C3 projects.
- **`activity_report` discovery tool** (read tier): auto-exposed on MCP, OpenAPI,
`POST /api/discovery/call`, and the internal Oracle chat. `narrate=true` adds a
best-effort LLM prose summary.
- **`GET /api/activity/digest`** endpoint (`date` / `since` / `until` / `project` /
`narrate` query params) + an **Activity** tab in the Oracle dashboard.

---

## v1.1.0 (2026-04-10)

### Ollama Cloud Migration
Expand Down
2 changes: 1 addition & 1 deletion oracle-guide/discovery-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Set in `~/.c3/oracle/config.json`:
**Discovery (read):** `list_projects`, `search_facts`, `query_memory`,
`project_health`, `analyze_project`, `cross_insights`, `read_graph`, `c3_search`,
`c3_search_cross`, `c3_read`, `c3_compress`, `c3_validate`, `c3_status`,
`c3_memory_query`, `c3_edits`, `c3_edits_cross`.
`c3_memory_query`, `c3_edits`, `c3_edits_cross`, `activity_report`.

**Safe actions:** `suggest_action` (creates a *pending* memory suggestion a human
approves), `delegate_task` (runs a configured Oracle agent).
Expand Down
108 changes: 108 additions & 0 deletions oracle/oracle.html
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@
<div class="tab active" data-tab="chat">Chat</div>
<div class="tab" data-tab="projects">Projects</div>
<div class="tab" data-tab="insights">Insights</div>
<div class="tab" data-tab="activity">Activity</div>
<div class="tab" data-tab="crossgraph">Cross-Graph</div>
<div class="tab" data-tab="suggestions">Suggestions</div>
<div class="tab" data-tab="agents">Team / Agents</div>
Expand Down Expand Up @@ -975,6 +976,27 @@ <h2 style="font-size:16px;font-weight:600">Cross-Project Insights</h2>
<div id="insightsEmpty" class="empty" style="display:none">No insights yet. Click "Generate" with 2+ projects.</div>
</div>

<!-- ── Activity ── -->
<div class="panel" id="panel-activity">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px">
<div>
<h2 style="font-size:16px;font-weight:600">Activity Digest</h2>
<p style="font-size:12px;color:var(--text2);margin-top:4px">Cross-project sessions, tool calls, edits, git mutations and token/cost for one day (UTC).</p>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="date" id="actDate" style="padding:4px 6px" onchange="loadActivity()">
<label style="font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px">
<input type="checkbox" id="actNarrate"> Narrate
</label>
<button class="btn btn-primary btn-sm" id="btnLoadActivity" onclick="loadActivity()">Refresh</button>
</div>
</div>
<div id="actSummary" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px"></div>
<div id="actNarrative" class="card" style="display:none;margin-bottom:14px"></div>
<div id="actProjects"></div>
<div id="actEmpty" class="empty" style="display:none">No activity recorded for this day.</div>
</div>

<!-- ── Cross-Graph ── -->
<div class="panel" id="panel-crossgraph">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:10px">
Expand Down Expand Up @@ -1279,6 +1301,9 @@ <h2 id="agentModalTitle" style="font-size:18px;margin-bottom:16px;font-weight:60
} else {
mainEl.classList.remove('wide');
}
if (tab.dataset.tab === 'activity' && !window._actLoaded) {
loadActivity(); window._actLoaded = true;
}
});
});

Expand Down Expand Up @@ -1899,6 +1924,89 @@ <h2 id="agentModalTitle" style="font-size:18px;margin-bottom:16px;font-weight:60
}, { successMsg: 'Insight dismissed', silent: false });
}

// ═══════════════════════════════════════════════════════════
// ── Activity Digest ──
// ═══════════════════════════════════════════════════════════
async function loadActivity() {
const dateEl = document.getElementById('actDate');
if (dateEl && !dateEl.value) dateEl.value = new Date().toISOString().slice(0, 10);
const date = dateEl ? dateEl.value : '';
const narrate = document.getElementById('actNarrate')?.checked ? 'true' : 'false';
const btn = document.getElementById('btnLoadActivity');
if (btn) btn.disabled = true;
try {
const qs = new URLSearchParams({ date, narrate }).toString();
renderActivity(await api('/api/activity/digest?' + qs));
} catch (e) {
renderActivity({ error: (e && e.message) || 'Failed to load activity.' });
} finally {
if (btn) btn.disabled = false;
}
}

function _actCard(label, value) {
return `<div class="card" style="flex:1;min-width:110px;text-align:center">
<div style="font-size:22px;font-weight:700">${value}</div>
<div style="font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em">${label}</div>
</div>`;
}

function renderActivity(data) {
const summary = document.getElementById('actSummary');
const projects = document.getElementById('actProjects');
const empty = document.getElementById('actEmpty');
const narrEl = document.getElementById('actNarrative');
if (!data || data.error) {
summary.innerHTML = ''; projects.innerHTML = ''; narrEl.style.display = 'none';
empty.style.display = ''; empty.textContent = (data && data.error) || 'Failed to load activity.';
return;
}
const t = data.totals || {};
summary.innerHTML = [
_actCard('Projects', t.projects_active || 0),
_actCard('Sessions', t.sessions || 0),
_actCard('Tool Calls', t.tool_calls || 0),
_actCard('Edits', t.edits || 0),
_actCard('Git', t.git_mutations || 0),
_actCard('Cost $', (t.cost_usd || 0).toFixed(2)),
].join('');

if (data.narrative) {
narrEl.style.display = '';
narrEl.innerHTML = `<div style="font-size:11px;color:var(--text2);text-transform:uppercase;margin-bottom:6px">${esc(data.window && data.window.label || '')}</div>
<p style="font-size:13px;line-height:1.6;white-space:pre-wrap">${esc(data.narrative)}</p>`;
} else {
narrEl.style.display = 'none';
}

const rows = data.projects || [];
if (!rows.length) {
projects.innerHTML = ''; empty.style.display = '';
empty.textContent = 'No activity recorded for ' + (data.window && data.window.label || 'this day') + '.';
return;
}
empty.style.display = 'none';
projects.innerHTML = `
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="text-align:left;color:var(--text2);font-size:11px;text-transform:uppercase">
<th style="padding:6px">Project</th><th style="padding:6px">Sessions</th>
<th style="padding:6px">Tools</th><th style="padding:6px">Edits</th>
<th style="padding:6px">Git</th><th style="padding:6px">Tokens</th>
<th style="padding:6px">Cost $</th><th style="padding:6px">Last activity</th>
</tr></thead>
<tbody>${rows.map(p => `<tr style="border-top:1px solid var(--border)">
<td style="padding:6px;font-weight:600">${esc(p.name)}</td>
<td style="padding:6px">${(p.sessions || []).length}</td>
<td style="padding:6px">${p.tool_calls || 0}</td>
<td style="padding:6px">${p.edits || 0}</td>
<td style="padding:6px">${p.git_mutations || 0}</td>
<td style="padding:6px">${(((p.tokens || {}).input || 0) + ((p.tokens || {}).output || 0)).toLocaleString()}</td>
<td style="padding:6px">${(p.cost_usd || 0).toFixed(2)}</td>
<td style="padding:6px;color:var(--text2);font-size:11px">${esc((p.last_activity || '').replace('T', ' ').slice(0, 19))}</td>
</tr>`).join('')}</tbody>
</table>`;
}

// ═══════════════════════════════════════════════════════════
// ── Suggestions ──
// ═══════════════════════════════════════════════════════════
Expand Down
26 changes: 25 additions & 1 deletion oracle/oracle_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from oracle.config import ORACLE_DIR, load_config, save_config # noqa: E402
from oracle.mcp_oracle import mcp_url, start_mcp_thread # noqa: E402
from oracle.services import api_auth # noqa: E402
from oracle.services.activity_reporter import ActivityReporter # noqa: E402
from oracle.services.api_auth import extract_bearer # noqa: E402
from oracle.services.api_auth import verify as verify_api_key # noqa: E402
from oracle.services.c3_bridge import C3Bridge # noqa: E402
Expand Down Expand Up @@ -57,10 +58,11 @@
_c3_bridge: C3Bridge | None = None
_federated: FederatedGraph | None = None
_tool_registry: ToolRegistry | None = None
_activity_reporter: ActivityReporter | None = None


def _init_services():
global _cfg, _bridge, _scanner, _reader, _checker, _writer, _cross_memory, _engine, _agent, _model_verified, _chat_store, _chat_engine, _c3_bridge, _federated, _tool_registry
global _cfg, _bridge, _scanner, _reader, _checker, _writer, _cross_memory, _engine, _agent, _model_verified, _chat_store, _chat_engine, _c3_bridge, _federated, _tool_registry, _activity_reporter
_cfg = load_config()
_bridge = OllamaBridge(
base_url=_cfg.get("ollama_base_url", "https://ollama.com"),
Expand Down Expand Up @@ -97,6 +99,7 @@ def _verify():
interval=int(_cfg.get("review_interval_seconds", 1800)),
federated_graph=_federated,
)
_activity_reporter = ActivityReporter(scanner=_scanner, ollama_bridge=_bridge)
_chat_engine = ChatEngine(
bridge=_bridge,
reader=_reader,
Expand All @@ -107,6 +110,7 @@ def _verify():
scanner=_scanner,
store=_chat_store,
c3_bridge=_c3_bridge,
activity_reporter=_activity_reporter,
)
_tool_registry = ToolRegistry(
ToolExecutor(_chat_engine),
Expand Down Expand Up @@ -608,6 +612,26 @@ def api_chat_conversation_state(conv_id):
return jsonify({"state": _chat_store.get_state(conv_id)})


# ── Activity digest (Oracle UI) ───────────────────────────
@app.route("/api/activity/digest", methods=["GET"])
def api_activity_digest():
"""Cross-project activity digest for the Oracle UI.

Query params: date=YYYY-MM-DD, since, until, project (single-project path),
narrate=true|false. Defaults to today (UTC) across all registered projects.
"""
if not _activity_reporter:
return jsonify({"error": "not initialized"}), 500
narrate = str(request.args.get("narrate", "")).lower() in ("1", "true", "yes", "on")
return jsonify(_activity_reporter.report(
date=request.args.get("date", ""),
since=request.args.get("since", ""),
until=request.args.get("until", ""),
project_path=request.args.get("project", ""),
narrate=narrate,
))


# ── Discovery API (external LLM tool surface) ─────────────
@app.route("/api/discovery/tools", methods=["GET"])
def api_discovery_tools():
Expand Down
Loading
Loading