Skip to content
Open
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
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- `packages/kaos`: the execution environment and file/process abstractions.
- `packages/oauth`: Kimi OAuth and managed auth utilities.
- `packages/telemetry`: shared client-side telemetry infrastructure.
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth).
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST (`/api/v1`) and the v3 flat entity message WebSocket protocol (`/api/v3/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth).
- `packages/remote-control`: the Kimi Remote Control tunnel client — registers this machine with the relay and forwards HTTP/WebSocket traffic to the local server, with a machine-wide single-instance lock; consumed by kap-server (the `/api/v1/remote-control` toggle) and by the CLI (`kimi web --remote-control`).
- `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 (`global.*` / `session(id).*` / `agent(id).*`, zod-validated); transport via subpath entry (`@moonshot-ai/klient/ipc|memory`, both return the same `Klient`); also hosts the e2e suites. See `packages/klient/AGENTS.md`.
- `packages/tree-sitter-bash`: a pure-TypeScript bash parser (no runtime deps, no wasm); `parse(source, { timeoutMs, maxNodes })` runs under a deterministic budget and returns a discriminated `ParseResult` — callers must treat aborted/hasError trees as "cannot analyze" and degrade. Parser only, no safety judgments; see the package README's "Known differences" section.
Expand All @@ -48,7 +47,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo

## General Coding Rules

- `packages/agent-core-v2`, `packages/kap-server`, and `packages/transcript` are comment-free zones: no comments of any kind — no line/block comments, no JSDoc (not even on exported symbols); the only exception is load-bearing lint-suppression directives (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs` over `.ts`/`.tsx`/`.mts`/`.mjs` under `src/`/`test/`/`scripts/`, which runs as part of `pnpm lint`.
- `packages/agent-core-v2` and `packages/kap-server` are comment-free zones: no comments of any kind — no line/block comments, no JSDoc (not even on exported symbols); the only exception is load-bearing lint-suppression directives (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs` over `.ts`/`.tsx`/`.mts`/`.mjs` under `src/`/`test/`/`scripts/`, which runs as part of `pnpm lint`.
- For optional object properties, pass `undefined` directly instead of using conditional spread.
- YES: `{ user }`
- NO: `{ ...(user ? { user } : undefined) }`
Expand Down
4 changes: 2 additions & 2 deletions apps/kimi-inspect/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A left icon rail (`src/components/NavRail.tsx`) switches top-level views:
- **Global message search** (`src/components/SearchView.tsx`) — cross-session full-text search over `POST /api/v1/search`, cursor-paged via a manual Load more; an exact-match checkbox maps to the API's `mode: 'literal'` substring search, which ignores sort and orders newest-first; a `live`/`index` badge on the results shows which server route served them (in-memory session transcript vs the persisted index).
- **Model Catalog** (`src/components/ModelCatalogView.tsx`) — every Provider with its Models and the default marker, via `IModelCatalog` / `IModelService` channel proxies, with per-model ping and session creation actions.
- **App Services** (`src/components/AppServicesView.tsx`) — the app-scope Service reflection, full width, joined by the **Workspace Services** view (`src/components/WorkspaceServicesView.tsx`) — the workspace-scope counterpart with a left sidebar directory browser (`src/components/WorkspaceDirBrowser.tsx` — server-side fs browsing over the App-scope `IHostFolderBrowser`, marking entries that are registered workspaces with their `IWorkspaceTrust` trust state, and registering a picked folder on demand via `IWorkspaceService.createOrTouch`), its proxies riding the `/workspace/:id` route, which materializes the handler on demand via `IWorkspaceLifecycleService.handlerFor`.
- **DI view** (`src/components/DiInspectionView.tsx`) — the engine's Service × Effect × DI debug surface over the App-scope `IDebugLedgerService` / `IDebugGraphService` / `IDebugEventsService` / `IDebugCascadeService`: the unit tree = ledger tree with unprovide / update / dispose triggers, the dependency DAG as Miller columns (`di/DiGraphPanel.tsx`), the event-subscription ledger (unit-book `on:<name>` entries + per-bus listener counts, `di/DiEventsPanel.tsx`), the cascade history, and the waiting area; the five panels poll on a short interval and refresh eagerly off the global `event.di.unit_changed` WS frame via `src/activity/di.ts`, which invalidates the `['di']` react-query prefix.
- **DI view** (`src/components/DiInspectionView.tsx`) — the engine's Service × Effect × DI debug surface over the App-scope `IDebugLedgerService` / `IDebugGraphService` / `IDebugEventsService` / `IDebugCascadeService`: the unit tree = ledger tree with unprovide / update / dispose triggers, the dependency DAG as Miller columns (`di/DiGraphPanel.tsx`), the event-subscription ledger (unit-book `on:<name>` entries + per-bus listener counts, `di/DiEventsPanel.tsx`), the cascade history, and the waiting area; the five panels poll on a short interval over the `/api/v1/debug` RPC (no push channel — a settled trigger invalidates the `['di']` react-query prefix so every panel converges).

The **Agent scope** stays in the Chat view's right dock (`src/components/RightPanel.tsx` — Audit / Agent / State / Session tabs) across two of them:

Expand All @@ -25,7 +25,7 @@ Built on its own old-klient-style channel layer (`src/channel/`: the VS Code `Pr

## Session activity

Session-level coarse status is the one exception to no-push: `src/activity/` holds a second `/api/v1/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global facts — `event.session.work_changed` updates a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while `event.session.created` / `event.session.archived` / `session.meta.updated` / `event.workspace.*` invalidate the `['sessions']` / `['v2-sessions']` / `['workspaces']` queries (an archive also drops the session's live activity entry, since no further `work_changed` frames will correct a stale badge); the session tree rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities` (live facts override the REST `activity.status`).
Session-level coarse status is the one exception to no-push: `src/activity/` holds a second `/api/v3/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global messages — the hub fans them out to every established connection with no subscribe frame — where every `session` message (created / updated / archived / deleted) embeds the full SessionInfo whose `busy` / `main_turn_active` / `pending_interaction` / `last_turn_reason` update a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while the message subtypes plus `workspace` created / updated / deleted invalidate the `['sessions']` / `['v2-sessions']` / `['workspaces']` queries (an archive or delete also drops the session's live activity entry, since no further messages will correct a stale badge); the session tree rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities` (live facts override the REST `activity.status`). Session/agent-grained traffic (`session.state`, entities, deltas) stays subscribe-gated server-side and never arrives on this socket; the DI view has no push feed at all — its panels poll the `/api/v1/debug` RPC.

## Dev server

Expand Down
8 changes: 4 additions & 4 deletions apps/kimi-inspect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ there is no fallback data source.
workspace handlers materialize on demand).
- **DI** — the engine's Service × Effect × DI debug surface, four panels fed
by the App-scope debug Services (`IDebugLedgerService` / `IDebugGraphService`
/ `IDebugCascadeService`) and refreshed eagerly off the `event.di.unit_changed`
WS frame:
/ `IDebugCascadeService`) over the `/api/v1/debug` RPC, polling on a short
interval:
- **Unit tree** — scope → unit → ledger entries (label, five-state
`Pending / Activating / Active / Unloading / Failed`, uid, `pinned` flag,
unit error object), with **unprovide / update / dispose** triggers.
Expand All @@ -53,5 +53,5 @@ there is no fallback data source.
`GET /api/v1/debug/channels` enumerates every scoped Service — there is no
whitelist; new Services appear automatically.
- There is no Service-event push channel besides the global events listed
above; panels fetch/refresh on demand (react-query, 15 s poll) plus the
`event.di.unit_changed` invalidation for the DI view.
above; panels fetch/refresh on demand (react-query, 15 s poll the DI view
polls on its own short interval).
53 changes: 0 additions & 53 deletions apps/kimi-inspect/src/activity/di.ts

This file was deleted.

122 changes: 68 additions & 54 deletions apps/kimi-inspect/src/activity/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,37 @@ function seedFetch(items: Record<string, unknown>[]): typeof fetch {
})) as unknown as typeof fetch;
}

function sessionMessage(
subtype: 'created' | 'updated' | 'archived' | 'deleted',
session: Record<string, unknown> & { id: string },
): Record<string, unknown> {
return {
type: 'session',
timestamp: Date.now(),
subtype,
session: {
workspace_id: 'wd_example_0123456789ab',
title: 'session',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
busy: false,
metadata: { cwd: '/tmp/example' },
agent_config: { model: 'test-model' },
usage: {
input_tokens: 0,
output_tokens: 0,
cache_read_tokens: 0,
cache_creation_tokens: 0,
context_tokens: 0,
},
permission_rules: [],
message_count: 0,
last_seq: 0,
...session,
},
};
}

describe('SessionActivityStore', () => {
it('applies work facts and notifies with a version bump', () => {
const store = new SessionActivityStore();
Expand Down Expand Up @@ -112,17 +143,13 @@ describe('SessionActivityHub', () => {

expect(hub.store.get('s1')).toEqual(facts({ busy: true, mainTurnActive: true }));
expect(hub.store.get('s2')?.pendingInteraction).toBe('approval');
// The hello goes out with no subscriptions — global facts flow regardless.
const hello = JSON.parse(instances[0]!.sent[0]!) as {
type: string;
payload: { subscriptions: string[] };
};
expect(hello.type).toBe('client_hello');
expect(hello.payload.subscriptions).toEqual([]);
// Nothing goes out — v3 global messages flow to every connection with no
// subscribe frame.
expect(instances[0]!.sent).toEqual([]);
hub.close();
});

it('applies live work_changed frames by session id', () => {
it('applies live session messages by session id', () => {
const { ctor, instances } = makeFakeWsCtor();
const hub = new SessionActivityHub({
url: 'http://127.0.0.1:58627',
Expand All @@ -132,25 +159,22 @@ describe('SessionActivityHub', () => {
});
instances[0]!.emit('open');

instances[0]!.emitFrame({
type: 'event.session.work_changed',
session_id: 's1',
payload: {
type: 'event.session.work_changed',
instances[0]!.emitFrame(
sessionMessage('updated', {
id: 's1',
busy: true,
main_turn_active: true,
pending_interaction: 'question',
last_turn_reason: null,
},
});
}),
);

expect(hub.store.get('s1')).toEqual(
facts({ busy: true, mainTurnActive: true, pendingInteraction: 'question' }),
);
hub.close();
});

it('forwards created and meta updates as list-level signals', () => {
it('forwards created and updated messages as list-level signals', () => {
const { ctor, instances } = makeFakeWsCtor();
const onListChanged = vi.fn();
const hub = new SessionActivityHub({
Expand All @@ -161,17 +185,17 @@ describe('SessionActivityHub', () => {
});
instances[0]!.emit('open');

instances[0]!.emitFrame({ type: 'event.session.created', session_id: 's1', payload: {} });
instances[0]!.emitFrame({ type: 'session.meta.updated', session_id: 's1', payload: {} });
// Agent-grained frames are ignored even if they somehow arrive.
instances[0]!.emitFrame(sessionMessage('created', { id: 's1' }));
instances[0]!.emitFrame(sessionMessage('updated', { id: 's1' }));
// Unknown future message types are ignored silently.
instances[0]!.emitFrame({ type: 'turn.started', session_id: 's1', payload: {} });

expect(onListChanged).toHaveBeenCalledTimes(2);
expect(hub.store.get('s1')).toBeUndefined();
expect(hub.store.get('s1')).toEqual(facts());
hub.close();
});

it('forwards archived and workspace frames as list-level signals and drops archived facts', () => {
it('forwards archived/deleted and workspace messages as list-level signals and drops gone facts', () => {
const { ctor, instances } = makeFakeWsCtor();
const onListChanged = vi.fn();
const hub = new SessionActivityHub({
Expand All @@ -182,46 +206,36 @@ describe('SessionActivityHub', () => {
});
instances[0]!.emit('open');

instances[0]!.emitFrame({
type: 'event.session.work_changed',
session_id: 's1',
payload: { type: 'event.session.work_changed', busy: true },
});
instances[0]!.emitFrame(sessionMessage('updated', { id: 's1', busy: true }));
expect(hub.store.get('s1')).toBeDefined();

// Global-dispatched frames carry the __global__ watermark; the real
// session id rides in the payload.
instances[0]!.emitFrame({
type: 'event.session.archived',
session_id: '__global__',
payload: { type: 'event.session.archived', sessionId: 's1', workspace_id: 'wd_1' },
});
instances[0]!.emitFrame(sessionMessage('archived', { id: 's1', archived: true }));
expect(hub.store.get('s1')).toBeUndefined();
expect(onListChanged).toHaveBeenCalledTimes(1);
expect(onListChanged).toHaveBeenCalledTimes(2);

instances[0]!.emitFrame({
type: 'event.session.work_changed',
session_id: 's2',
payload: { type: 'event.session.work_changed', busy: true },
});
instances[0]!.emitFrame(sessionMessage('updated', { id: 's2', busy: true }));
expect(hub.store.get('s2')).toBeDefined();

instances[0]!.emitFrame({
type: 'event.session.deleted',
session_id: '__global__',
payload: { type: 'event.session.deleted', sessionId: 's2', workspace_id: 'wd_1' },
});
instances[0]!.emitFrame(sessionMessage('deleted', { id: 's2' }));
expect(hub.store.get('s2')).toBeUndefined();
expect(onListChanged).toHaveBeenCalledTimes(2);

for (const type of [
'event.workspace.created',
'event.workspace.updated',
'event.workspace.deleted',
]) {
instances[0]!.emitFrame({ type, session_id: '__global__', payload: {} });
expect(onListChanged).toHaveBeenCalledTimes(4);

for (const subtype of ['created', 'updated', 'deleted']) {
instances[0]!.emitFrame({
type: 'workspace',
timestamp: Date.now(),
subtype,
workspace: {
id: 'wd_example_0123456789ab',
root: '/tmp/example',
name: 'example',
created_at: new Date().toISOString(),
last_opened_at: new Date().toISOString(),
session_count: 0,
},
});
}
expect(onListChanged).toHaveBeenCalledTimes(5);
expect(onListChanged).toHaveBeenCalledTimes(7);
hub.close();
});
});
Loading
Loading