Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9a68ea0
docs: record Activity backend roadmap
lidge-jun Sep 5, 2026
4afdd76
feat: expose shared Activity presentation contracts
lidge-jun Sep 5, 2026
a54c8dd
feat: bind Activity snapshots to resolved chat identity
lidge-jun Sep 5, 2026
f8dc700
feat: persist Activity display without execution side effects
lidge-jun Sep 5, 2026
7bdfffa
docs: describe Activity identity and display settings
lidge-jun Sep 5, 2026
0f98640
docs: checkpoint verified Activity foundation
lidge-jun Sep 5, 2026
52110fb
feat: persist bounded owner-bound Activity journal and replay
lidge-jun Sep 5, 2026
494aaf0
feat: capture chat owners at all trace admissions
lidge-jun Sep 5, 2026
9700158
docs: document Activity replay ownership and retention
lidge-jun Sep 5, 2026
29c86df
docs: reconcile backend and parent evidence ancestry
lidge-jun Sep 5, 2026
5058b38
Merge commit 'd5b9310dc' into codex/native-activity-journal
lidge-jun Sep 5, 2026
f0b7867
fix(web): capture trace session across all drawer reads
lidge-jun Sep 5, 2026
64ffe52
docs: reconcile current parent protocol evidence
lidge-jun Sep 5, 2026
147b187
Merge commit '733560039' into codex/native-activity-journal
lidge-jun Sep 5, 2026
560f0e1
feat: observe print Activity with captured presentation identity
lidge-jun Sep 5, 2026
ebb5419
feat: expose existing trace identity allocator
lidge-jun Sep 5, 2026
d2b4d4a
test: verify display-only settings through runtime behavior
lidge-jun Sep 5, 2026
d088828
feat: retain accepted print activity and durable tool updates
lidge-jun Sep 5, 2026
8611a81
fix: merge latest tool state without blocking final delivery
lidge-jun Sep 5, 2026
c3289bd
feat: connect print activity across spawn and failure paths
lidge-jun Sep 5, 2026
8e76f6b
fix: preserve recovered Claude tool identity after read failure
lidge-jun Sep 5, 2026
95a79ff
test: drive tool log boundaries through lifecycle and HTTP
lidge-jun Sep 5, 2026
e9b4692
fix: enrich missing metadata on terminal runtime tools
lidge-jun Sep 5, 2026
667573d
fix: preserve terminal tool results under preview budget pressure
lidge-jun Sep 5, 2026
3bd1201
feat: expose published runtime item identity lookup
lidge-jun Sep 5, 2026
4ed5f33
fix: allow explicit print terminal tool refreshes
lidge-jun Sep 5, 2026
4a6ad70
test: isolate shared projection regression fixtures
lidge-jun Sep 5, 2026
a22fd8f
docs: describe print Activity and final delivery boundaries
lidge-jun Sep 5, 2026
fbc2952
test: keep native spawn fixture aligned with trace recovery API
lidge-jun Sep 5, 2026
4fdac22
merge: reconcile current parent API inventory with Activity routes
lidge-jun Sep 5, 2026
6f1fba6
test: verify reentrant spawn error settlement behavior
lidge-jun Sep 5, 2026
2a043b1
docs: archive completed Activity backend evidence
lidge-jun Sep 5, 2026
422cbb7
merge: cascade verified native Activity parent into backend
lidge-jun Sep 5, 2026
7f16d31
fix: bind native Cursor trace to captured chat owner
lidge-jun Sep 5, 2026
812937d
test: compose native Cursor fixtures with owned Activity journal
lidge-jun Sep 5, 2026
f1ebbfb
test: preserve captured print binding across parent cascade
lidge-jun Sep 5, 2026
f0ce374
merge: include parent gateway behavior regression checks
lidge-jun Sep 5, 2026
43d05fc
docs: record completed Cursor journal composition
lidge-jun Sep 5, 2026
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
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ git add devlog && git commit -m "chore: update devlog ref" && git push

### Architecture Docs Sync

- Native event foundation: `src/shared/runtime-contract.ts` + `src/agent/runtime/*` own canonical Codex/Pi projections and optional explicit outcomes. `agent_runtime`/`agent_runtime_gap` publish directly to SSE, bypassing messaging listeners. Native compatibility terminals carry only finality/status and existing trace identity; no public partial/outcome object. Preserve legacy final selection when outcome is absent, and interrupted MESSAGE salvage before exit settlement. See `structure/runtime-integration.md` and `structure/stream-events.md`; Activity layout/default settings are separate follow-on work.
- Native event foundation: `src/shared/runtime-contract.ts` + `src/agent/runtime/*` own canonical Codex/Pi projections and optional explicit outcomes. `agent_runtime`/`agent_runtime_gap` publish directly to SSE, bypassing messaging listeners. Native compatibility terminals carry only finality/status and existing trace identity; no public partial/outcome object. Preserve legacy final selection when outcome is absent, and interrupted MESSAGE salvage before exit settlement. See `structure/runtime-integration.md` and `structure/stream-events.md`; display defaults and durable history use `shared/presentation.ts` and `trace/activity-journal.ts`, with client layout handled separately.
- Runtime selection: only Cursor/Grok/Claude accept `perCli.<cli>.transport`; existing absence stays print before defaults merge. Native switchable keys prefix the whole legacy bucket with `native-v1:` and never overwrite the print singleton. Capture transport/bucket once, forward through lifecycle persistence/compact, and keep scoped resets exact. Unsupported main/worker native adapters fail before print/fallback work; compiled support in `/api/cli-status` is separate from cached auth/binary readiness. Codex App/Pi keys remain unchanged.

- Cursor native main: explicit native/auto only; restrictive permissions and unsupported workers fail before `regenerateB`, bucket/bootstrap/snapshot, detection or pool work. `AcpRuntimeSession` keeps raw final/partial independent of bounded Activity and claims an immutable logical result before lifecycle; passive finalization survives main-map removal. Native run cleanup holds the exact lease through application settlement and uses captured exit-barrier identity. Server-explicit scope/chat bindings survive multi-session off; private identity-only I/O liveness keeps the owning collector alive without messaging content. Preserve legacy/manual compact while excluding print-era automatic compact/count heuristics from explicit native outcomes.
Expand All @@ -194,6 +194,12 @@ git add devlog && git commit -m "chore: update devlog ref" && git push

### Native decisions

Print Activity observes accepted legacy parser data and closes from the existing lifecycle-selected application-final. Copilot/ordinary print own observers; native Pi/Codex do not create a second one. Captured `activityIdentity` supplies existing sessionId/scope wire fields even with multi-session disabled. Tool merges use run+ref/seq identity, terminal non-regression and one omission marker; safe trace-tool recovery must not throw into provider parsing. Spawn error/stale-retry bypasses close the observer and trace independently while preserving existing send/resolve/retry order.

Activity journal and raw trace routes share exact chat ownership for every owned row, including historical backfills. Forked message pointers do not grant access. Runtime rows are immutable and bounded; whole-prefix retention reports loss, protects active owners and cannot be disabled by one corrupt control. Journal failure must not interrupt final delivery or MESSAGE salvage. See `structure/runtime-integration.md` and `structure/server_api.md`.

Activity display is selected by `presentation.mode` (`activity` default, explicit `legacy` retained), separately from provider transport. Snapshot `GET /api/orchestrate/snapshot?session=...` supplies captured `activityIdentity={sessionId,scope}`; clients validate it before semantic admission. Presentation-only settings writes must not reset fallback state or synchronize execution configuration. Existing instance auth and disabled multi-session resolver policy remain.

`src/agent/runtime/requests.ts` and `acp/callbacks.ts` own bounded pending decisions, opaque native-option mapping and cancellation latches. `GET /api/runtime/requests?sessionId=...` and `POST /api/runtime/requests/:id` use the existing instance auth policy (including loopback/LAN bypass), never a current-session fallback. Match run/session/scope/turn and current ownership before answering. Canonical sanitization and the32KiB event preflight precede insertion; global128/120s and per-connection32 bounds apply. An admitted selected write cannot be retracted: cancellation during dispatch retires the connection. Provider activation, approval UI and messaging changes are not implied. Sync `structure/runtime-integration.md` and `structure/server_api.md`.

### Concurrent inbound gateway (M1)
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ This repository is a Node.js ESM orchestration runtime for boss/employee dispatc

## Documentation Map

- Print Activity is an observation path: `runtime/print-projection.ts` and `print-activity.ts` reuse canonical projection bounds and gap handling. Legacy accepted-event hooks run before message resets; selected application-final comes only from lifecycle. `merge-tool-log.ts` keys by run plus stable ref/seq and preserves terminal status, identityless tools and one omission marker. Trace-only failures cannot roll back a MESSAGE or introduce another send.

- Activity journal reuses immutable runtime trace rows and nullable admission owners. Replay and raw owned trace reads require exact chat ownership; forks do not inherit access. Whole-prefix retention preserves explicit loss while active owners remain protected. Journal failure cannot gate final delivery or interrupted MESSAGE salvage.

- Activity uses `presentation.mode` (`activity` by default, reversible `legacy`) independently of provider transport. `GET /api/orchestrate/snapshot?session=...` returns server-owned `activityIdentity`; validate it before semantic admission. Display-only writes preserve runtime selection and delivery. See `structure/runtime-integration.md`.

- Start at `structure/INDEX.md` for the current architecture map.
- Keep `README.md`, `AGENTS.md`, this file, and `structure/AGENTS.md` aligned when command/API/orchestration behavior changes. Concurrent inbound gateway changes belong in `structure/INDEX.md`, `structure/infra.md`, `structure/telegram.md`, and the messaging runtime docs.
- Do not use the old `devlog/structure/` path for architecture docs; the active folder is `structure/`.
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@

## Install

Activity-capable clients use `presentation.mode`: `activity` by default, or `legacy`
for the previous transcript view. This preference is independent of provider transport.
Activity clients obtain their chat identity from the server snapshot before subscribing
to semantic updates; see [runtime integration](structure/runtime-integration.md).
Activity history is retained in the bounded trace journal and replayed with a fixed
cursor. Owned raw trace requests also carry their captured chat session.
Print runtimes project accepted messages and tool updates into the same journal;
the existing lifecycle still selects the final answer, including when Activity fails.

<details>
<summary><b>Safe install</b> — for existing users who want minimal changes</summary>

Expand Down
2 changes: 1 addition & 1 deletion devlog
14 changes: 13 additions & 1 deletion public/js/features/process-block.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { escapeHtml } from '../render.js';
import { ICONS } from '../icons.js';
import { api } from '../api.js';
import { parseActivityIdentity } from '../../../src/shared/presentation.js';
import { withCurrentSessionQuery } from './session-hub.js';
import {
displayShellCommand,
displayShellCommandDetail,
Expand Down Expand Up @@ -51,6 +54,7 @@ const PROCESS_DETAIL_COLLAPSE_CLEAR_CHARS = 1000;
const PROCESS_BLOCK_MAX_RENDERED_STEPS = 80;
const PROCESS_BLOCK_HEAD_STEPS = 24;
const PROCESS_BLOCK_TAIL_STEPS = 24;
let traceOpenIntent = 0;

export interface StoredProcessStepMeta {
id: string;
Expand Down Expand Up @@ -421,7 +425,15 @@ export function bindProcessBlockInteractions(root: HTMLElement): void {
event.stopPropagation();
const runId = traceTrigger.dataset['traceRunId'] || '';
const seq = Number(traceTrigger.dataset['traceSeq'] || 0);
import('./trace-drawer.js').then(m => m.openTraceDrawer(runId, seq))
const intent = ++traceOpenIntent;
// Capture the selected query before the lazy import; the server resolves
// disabled/default sessions too. A failed read never invents an owner.
const snapshot = api<{ activityIdentity?: unknown }>(withCurrentSessionQuery('/api/orchestrate/snapshot'));
Promise.all([import('./trace-drawer.js'), snapshot])
.then(([m, data]) => {
if (intent !== traceOpenIntent || !traceTrigger.isConnected) return;
return m.openTraceDrawer(runId, seq, parseActivityIdentity(data?.activityIdentity)?.sessionId ?? null);
})
.catch(error => console.warn('[trace-drawer] open failed:', error));
return;
}
Expand Down
35 changes: 25 additions & 10 deletions public/js/features/trace-drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ let totalCount = 0;
let loading = false;
let openRequestId = 0;
let selectedSeq: number | null = null;
let traceSession: string | null = null;
let traceController: AbortController | null = null;
let returnFocus: HTMLElement | null = null;

function eventTypeOf(event: TraceEventListItem): string { return event.eventType || event.event_type || 'event'; }
function isCurrentRequest(requestId: number, runId = currentRunId): boolean {
return requestId === openRequestId && runId === currentRunId;
}

function requestedOffset(seq?: number): number {
if (!seq || !Number.isInteger(seq) || seq < 1) return 0;
return Math.floor((seq - 1) / PAGE_SIZE) * PAGE_SIZE;
function tracePath(path: string): string {
if (!traceSession) return path;
return path + (path.includes('?') ? '&' : '?') + new URLSearchParams({session:traceSession});
}

function ensureDrawer(): HTMLElement {
Expand Down Expand Up @@ -74,7 +77,14 @@ function setRaw(text: string): void {
const raw = document.getElementById('traceEventRaw');
if (raw) raw.textContent = text;
}
function closeTraceDrawer(): void { document.getElementById('traceDrawerOverlay')?.classList.remove('open'); }
export function closeTraceDrawer(): void {
++openRequestId;
traceController?.abort(); traceController = null;
loading = false;
document.getElementById('traceDrawerOverlay')?.classList.remove('open');
if (returnFocus?.isConnected) returnFocus.focus({preventScroll:true});
returnFocus = null;
}

function renderSummary(summary: TraceSummary): void {
const title = document.getElementById('traceDrawerTitle');
Expand Down Expand Up @@ -121,9 +131,9 @@ function renderEventRows(events: TraceEventListItem[], runId: string): void {
async function loadNextPage(requestId = openRequestId, runId = currentRunId, offset = loadedCount): Promise<void> {
if (!runId || loading || (loadedCount >= totalCount && totalCount > 0 && offset >= loadedCount)) return;
loading = true;
const page = await api<TraceEventsPage>(`/api/traces/${encodeURIComponent(runId)}/events?offset=${offset}&limit=${PAGE_SIZE}`);
loading = false;
const page = await api<TraceEventsPage>(tracePath(`/api/traces/${encodeURIComponent(runId)}/events?offset=${offset}&limit=${PAGE_SIZE}`), {signal:traceController?.signal ?? null});
if (!isCurrentRequest(requestId, runId)) return;
loading = false;
if (!page) {
if (!selectedSeq) setRaw('Trace events could not be loaded.');
return;
Expand All @@ -139,15 +149,19 @@ async function loadEventDetail(runId: string, seq: number, requestId = openReque
if (!runId || !Number.isInteger(seq) || seq < 1) return;
if (!isCurrentRequest(requestId, runId)) return;
setRaw('Loading event...');
const detail = await api<TraceEventDetail>(`/api/traces/${encodeURIComponent(runId)}/events/${seq}`);
const detail = await api<TraceEventDetail>(tracePath(`/api/traces/${encodeURIComponent(runId)}/events/${seq}`), {signal:traceController?.signal ?? null});
if (!isCurrentRequest(requestId, runId) || selectedSeq !== seq) return;
setRaw(detail?.raw || (detail ? '(empty trace event)' : 'Trace event could not be loaded.'));
}

export async function openTraceDrawer(runId: string, seq?: number): Promise<void> {
export async function openTraceDrawer(runId: string, seq?: number, sessionId: string | null = null): Promise<void> {
const overlay = ensureDrawer();
const requestId = ++openRequestId;
const startOffset = requestedOffset(seq);
// Sequence is a sparse trace identity, not an ordinal in the retained row list.
const startOffset = 0;
traceController?.abort(); traceController = new AbortController();
traceSession = sessionId;
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
currentRunId = runId;
loadedCount = startOffset;
totalCount = 0;
Expand All @@ -157,7 +171,8 @@ export async function openTraceDrawer(runId: string, seq?: number): Promise<void
if (list) list.innerHTML = '';
setRaw('Loading trace...');
overlay.classList.add('open');
const summary = await api<TraceSummary>(`/api/traces/${encodeURIComponent(runId)}`);
overlay.querySelector<HTMLElement>('.trace-drawer-close')?.focus({preventScroll:true});
const summary = await api<TraceSummary>(tracePath(`/api/traces/${encodeURIComponent(runId)}`), {signal:traceController.signal});
if (!isCurrentRequest(requestId, runId)) return;
if (!summary) { setRaw('Trace is unavailable or internal-only.'); return; }
renderSummary(summary);
Expand Down
15 changes: 9 additions & 6 deletions src/agent/events/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { appendBoundedFullText } from './fulltext-bound.js';
import { fieldString } from '../../types/cli-events.js';
import { updateTraceToolRow, getTraceEvent } from '../../trace/store.js';
import { updateTraceToolRow, getTraceToolEntry } from '../../trace/store.js';
import type { CliEventRecord } from './types.js';
import type { SpawnContext, ToolEntry } from './types.js';
import {
Expand Down Expand Up @@ -147,6 +147,7 @@ function appendClaudeISnapshotText(ctx: SpawnContext, event: CliEventRecord): st
const previous = ctx.claudeILastAssistantText || '';
ctx.claudeILastAssistantText = text;
if (text === previous || previous.startsWith(text)) return '';
ctx.printActivity?.message(text, 'replace', 'unknown');
if (text.startsWith(previous)) {
const delta = text.slice(previous.length);
{
Expand Down Expand Up @@ -176,6 +177,7 @@ function appendClaudeISnapshotText(ctx: SpawnContext, event: CliEventRecord): st
if (messageId) ctx.claudeILastAssistantId = messageId;
else delete ctx.claudeILastAssistantId;
ctx.claudeILastAssistantText = text;
ctx.printActivity?.message(text, 'replace', 'unknown');
return appendAssistantTextSegment(ctx, text);
}

Expand All @@ -192,6 +194,7 @@ function appendClaudeISnapshotText(ctx: SpawnContext, event: CliEventRecord): st
* (spawn.ts gates that to kiro-plain/agy/pi), and this mirrors the same defensive
* check the reconcile path already carries. */
export function resetClaudeDurableMessage(ctx: SpawnContext): void {
ctx.printActivity?.nextMessage();
ctx.fullText = '';
if (ctx.liveOutputText !== undefined) ctx.liveOutputText = '';
ctx.outputTextStarted = false;
Expand Down Expand Up @@ -264,6 +267,7 @@ export function handleClaudeEvent(
const hasCanonicalText = evt.message.content.some(
(block) => block.type === 'text' && block.text,
);
if (hasCanonicalText) ctx.printActivity?.message(extractAssistantText(evt), 'replace', 'unknown');
if (ctx.claudeStreamedTextStart !== undefined && hasCanonicalText) {
const useLive = ctx.liveOutputText !== undefined;
const target = useLive ? ctx.liveOutputText! : ctx.fullText;
Expand Down Expand Up @@ -297,6 +301,7 @@ export function handleClaudeEvent(
if (fallbackId) ctx.claudeILastAssistantId = fallbackId;
for (const block of evt.message.content) {
if (block.type === 'text') {
if (block.text) ctx.printActivity?.message(block.text, 'append', 'unknown');
const segment = appendAssistantTextSegment(ctx, block.text);
ctx.pendingOutputChunk = (ctx.pendingOutputChunk || '') + segment;
}
Expand Down Expand Up @@ -339,16 +344,13 @@ export function handleClaudeEvent(
// status (WP4, devlog 260703 doc 12 item 3).
const pointer = ctx.toolTraceIndex?.get(`claude:tooluse:${block.tool_use_id}`);
if (pointer) {
let base: Partial<ToolEntry> = {};
const row = getTraceEvent(pointer.traceRunId, pointer.traceSeq);
if (row?.raw) {
try { base = JSON.parse(row.raw) as Partial<ToolEntry>; } catch { /* keep minimal base */ }
}
const base = getTraceToolEntry(pointer.traceRunId, pointer.traceSeq);
const resultText = extractText(block.content);
const merged: ToolEntry = {
toolType: 'tool',
label: 'tool',
...base,
stepRef: `claude:tooluse:${block.tool_use_id}`,
icon: block["is_error"] ? '❌' : '✅',
status: block["is_error"] ? 'error' : 'done',
traceRunId: pointer.traceRunId,
Expand All @@ -360,6 +362,7 @@ export function handleClaudeEvent(
: resultText;
}
updateTraceToolRow(merged);
ctx.printActivity?.tool(merged);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/agent/events/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export function handleCodexEvent(
if (evt.item?.type === 'agent_message') {
const text = String(evt.item.text || '');
const channel = evt.item?.['channel'] || (evt.item?.['annotations'] as Record<string, unknown> | undefined)?.['channel'];
ctx.printActivity?.nextMessage();
ctx.printActivity?.message(text, 'replace',
channel === 'final' ? 'final' : channel === 'commentary' ? 'commentary' : 'unknown');
// Commentary-channel messages are transient progress updates — do NOT
// persist them in fullText so they stay out of agent_done and therefore
// out of Slack/Telegram/Discord delivery.
Expand Down
Loading
Loading