Skip to content

Commit f7b490a

Browse files
NagyViktNagyVikt
andauthored
feat(awareness): unified liveness + working-note now lines (#592)
* feat(awareness): unified liveness + working-note now lines Two disconnected liveness systems (heartbeat files vs SQLite observations) meant a dead heartbeat writer hid a live agent. readHivemind now reconciles both when given a sqliteLiveness source and stamps liveness_source per session. attention_inbox gains active_working_notes — each other live session's latest task_note_working note (30-min window) — and the SessionStart preface shows up to 3 'now:' lines so agents start knowing what everyone else is mid-flight on. No storage migration. * fix(awareness): review follow-ups — normalize note previews, liveness parity everywhere Working-note previews are whitespace-normalized at the source so multi-line content cannot break single-line renderers. bridge_status, startup_panel, and the worker viewer now pass sqliteLiveness too — a SQLite-alive lane must not show dead in one surface and alive in another. --------- Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 3b86d74 commit f7b490a

19 files changed

Lines changed: 447 additions & 14 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@colony/core': minor
3+
'@colony/mcp-server': minor
4+
'@colony/hooks': minor
5+
---
6+
7+
Cross-agent awareness: unified liveness + working-note "now lines". `readHivemind` accepts an optional `sqliteLiveness` source — sessions with stale heartbeat files but fresh SQLite observations are reclassified as working (`liveness_source: 'sqlite'`); every hivemind session now carries `liveness_source`. `attention_inbox` gains `active_working_notes` (latest task_note_working note per other live session, 30-min window) plus `summary.active_working_note_count`. The SessionStart task preface shows up to 3 co-participant "now:" lines so agents start each session knowing what everyone else is mid-flight on.

apps/mcp-server/src/tools/bridge.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,15 @@ export async function buildBridgeStatusPayload(
122122
store: MemoryStore,
123123
{ session_id, agent, repo_root, branch, query }: BridgeStatusOptions,
124124
): Promise<BridgeStatus> {
125-
const snapshot = readHivemind(
126-
toHivemindOptions({
125+
const snapshot = readHivemind({
126+
...toHivemindOptions({
127127
repo_root,
128128
repo_roots: undefined,
129129
include_stale: true,
130130
limit: BRIDGE_LANE_LIMIT,
131131
}),
132-
);
132+
sqliteLiveness: store.storage,
133+
});
133134
const attentionInbox = buildAttentionInbox(store, {
134135
session_id,
135136
agent,

apps/mcp-server/src/tools/hivemind.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function register(server: McpServer, ctx: ToolContext): void {
5757
if (repo_roots !== undefined) options.repoRoots = repo_roots;
5858
if (include_stale !== undefined) options.includeStale = include_stale;
5959
if (limit !== undefined) options.limit = limit;
60+
options.sqliteLiveness = store.storage;
6061
const snapshot = readHivemind(options);
6162
return { content: [{ type: 'text', text: JSON.stringify(snapshot) }] };
6263
}),
@@ -103,9 +104,12 @@ export function register(server: McpServer, ctx: ToolContext): void {
103104
const attentionIdentity = resolveAttentionIdentity(session_id, agent);
104105
const laneLimit =
105106
limit ?? (localMode ? DEFAULT_LOCAL_CONTEXT_LANE_LIMIT : DEFAULT_CONTEXT_LANE_LIMIT);
106-
const snapshot = readHivemind(
107-
toHivemindOptions({ repo_root, repo_roots, include_stale, limit: laneLimit }),
108-
);
107+
const snapshot = readHivemind({
108+
...toHivemindOptions({ repo_root, repo_roots, include_stale, limit: laneLimit }),
109+
// Unified liveness: heartbeat-stale lanes with fresh SQLite
110+
// observations come back as working (liveness_source: 'sqlite').
111+
sqliteLiveness: store.storage,
112+
});
109113
const memoryLimit = memory_limit ?? DEFAULT_CONTEXT_MEMORY_LIMIT;
110114
const maxClaims = max_claims ?? DEFAULT_CONTEXT_CLAIM_LIMIT;
111115
const maxHotFiles = max_hot_files ?? DEFAULT_CONTEXT_HOT_FILE_LIMIT;

apps/mcp-server/src/tools/startup-panel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export async function buildStartupPanel(
151151
const snapshot = readHivemind({
152152
...(args.repo_root !== undefined ? { repoRoot: args.repo_root } : {}),
153153
limit: STARTUP_LANE_LIMIT,
154+
sqliteLiveness: store.storage,
154155
});
155156
const activeTask = resolveActiveTask(store, args);
156157
const scopedRepoRoot = args.repo_root ?? activeTask?.repo_root ?? null;

apps/mcp-server/test/autopilot.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ function makeInbox(overrides: Partial<AttentionInbox> = {}): AttentionInbox {
186186
recent_other_claim_count: 0,
187187
live_file_contention_count: 0,
188188
hot_file_count: 0,
189+
active_working_note_count: 0,
189190
omx_runtime_warning_count: 0,
190191
blocked: false,
191192
next_action: 'Idle.',
@@ -204,6 +205,7 @@ function makeInbox(overrides: Partial<AttentionInbox> = {}): AttentionInbox {
204205
recent_other_claims: [],
205206
live_file_contentions: [],
206207
file_heat: [],
208+
active_working_notes: [],
207209
omx_runtime_warnings: [],
208210
...overrides,
209211
};

apps/worker/src/server.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function buildApp(
5656
options: WorkerAppOptions = {},
5757
): Hono {
5858
const app = new Hono();
59-
const readCachedHivemind = createHivemindReader(options);
59+
const readCachedHivemind = createHivemindReader(store, options);
6060
const reportBuilder = options.discrepancyReportBuilder ?? buildDiscrepancyReport;
6161

6262
app.use('*', async (_c, next) => {
@@ -565,20 +565,28 @@ function safeJsonObject(raw: string | null): Record<string, unknown> {
565565
}
566566
}
567567

568-
function createHivemindReader(options: WorkerAppOptions): () => ReturnType<typeof readHivemind> {
568+
function createHivemindReader(
569+
store: MemoryStore,
570+
options: WorkerAppOptions,
571+
): () => ReturnType<typeof readHivemind> {
569572
let cached: ReturnType<typeof readHivemind> | null = null;
570573
let cachedAt = 0;
571574
return () => {
572575
const now = Date.now();
573576
if (cached && now - cachedAt < HIVEMIND_CACHE_TTL_MS) return cached;
574-
cached = readWorkerHivemind(options);
577+
cached = readWorkerHivemind(store, options);
575578
cachedAt = now;
576579
return cached;
577580
};
578581
}
579582

580-
function readWorkerHivemind(options: WorkerAppOptions): ReturnType<typeof readHivemind> {
581-
const input: HivemindOptions = { limit: 20 };
583+
function readWorkerHivemind(
584+
store: MemoryStore,
585+
options: WorkerAppOptions,
586+
): ReturnType<typeof readHivemind> {
587+
// Same unified liveness as the MCP hivemind tools — the viewer must not
588+
// show a SQLite-alive lane as dead.
589+
const input: HivemindOptions = { limit: 20, sqliteLiveness: store.storage };
582590
if (options.hivemindRepoRoots?.length) {
583591
input.repoRoots = options.hivemindRepoRoots;
584592
}

docs/mcp.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,8 @@ progressive-disclosure IDs should still call `hivemind_context`,
594594
Return live lane ownership plus compact relevant memory hits and negative warnings in one request.
595595
Before editing, inspect ownership, then claim touched files on the active task.
596596

597+
Lane liveness is unified across both liveness systems: each session carries `liveness_source` (`heartbeat` | `sqlite` | `worktree-lock` | `file-lock` | `managed-worktree`). A lane whose heartbeat file went stale but whose session wrote a SQLite observation within the heartbeat window is reclassified as `working` with `liveness_source: "sqlite"` — a dead heartbeat writer no longer hides a live agent.
598+
597599
```json
598600
{
599601
"name": "hivemind_context",
@@ -1521,6 +1523,8 @@ Errors include `{ "code": "SESSION_NOT_FOUND", "error": "..." }` when either `ta
15211523

15221524
Compact post-`hivemind_context` attention check for live pending handoffs, unread messages, blockers, stalled lanes, pending wakes, recent other-session file claims, stale claim cleanup signals, and decaying hot files. Expired handoffs are hidden from the pending bucket; the original observations remain available via timeline/search for audit. This is the main surface where `task_message` items show up: expired unread messages are hidden, read/replied messages stop triggering attention, and blocking messages remain prominent until read, replied, retracted, or expired. Use `task_messages` for a focused message-only inbox. Review compact IDs first, then fetch full bodies via `get_observations` only for the entries you need.
15231525

1526+
The payload includes `active_working_notes`: the latest `task_note_working` note from each other live session on your in-scope tasks (30-minute window, 120-char preview, one entry per session, hydrate via `get_observations`). `summary.active_working_note_count` carries the count — this is the "what is everyone doing right now" surface.
1527+
15241528
```json
15251529
{
15261530
"name": "attention_inbox",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-06-12
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Why
2+
3+
- Colony had two disconnected liveness systems — `.omx/state/active-sessions/` heartbeat files and the SQLite observations DB — so a dead heartbeat writer hid a live agent. And there was no "what is everyone doing right now" surface: agents had to hydrate timelines to learn what co-participants were mid-flight on.
4+
5+
## What Changes
6+
7+
- `readHivemind` accepts `sqliteLiveness` (satisfied by `MemoryStore.storage`); heartbeat-stale sessions with a SQLite observation inside the heartbeat window reclassify to `working` with new field `liveness_source: 'sqlite'`. All sessions carry `liveness_source` (`heartbeat`/`sqlite`/`worktree-lock`/`file-lock`/`managed-worktree`). MCP `hivemind` + `hivemind_context` pass the store.
8+
- `buildAttentionInbox` gains `active_working_notes[]` — the latest `task_note_working` note (kind `note` + `metadata.working_note`) per other session per in-scope task, 30-min window, 120-char preview — plus `summary.active_working_note_count`. Reuses `taskObservationsByKind`; **no storage migration**.
9+
- SessionStart task preface renders up to 3 co-participant `now:` lines (80-char) after the Joined-with line.
10+
11+
## Impact
12+
13+
- All additive. The Rust/codex bridge consuming hivemind payloads sees a new field; non-strict parsers unaffected.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Unified session liveness
4+
Hivemind snapshots SHALL reconcile heartbeat-file liveness with SQLite observation freshness when a `sqliteLiveness` source is provided, exposing `liveness_source` per session.
5+
6+
#### Scenario: SQLite-fresh session resurrected
7+
- **WHEN** a session's heartbeat is stale but its last observation is within the heartbeat window
8+
- **THEN** the session reports `activity: working` and `liveness_source: 'sqlite'`.
9+
10+
#### Scenario: Best-effort on storage failure
11+
- **WHEN** the liveness source throws
12+
- **THEN** the snapshot still returns with heartbeat-derived activity.
13+
14+
### Requirement: Working-note awareness surface
15+
`attention_inbox` SHALL surface the latest working note per other live session per in-scope task within a 30-minute window, and the SessionStart task preface SHALL render up to 3 such notes.
16+
17+
#### Scenario: Inbox working notes
18+
- **WHEN** another session posted a `working_note` observation within 30 minutes
19+
- **THEN** `active_working_notes` contains one entry for that session with task_id, observation_id, agent, ts, and a 120-char preview
20+
- **AND** the caller's own notes and non-working notes are excluded.

0 commit comments

Comments
 (0)