trajectories 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1219 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +120 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +19 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import type { Diagnostic } from "@letta-ai/trajectory";
|
|
6
|
+
import type { DiscoveredSession } from "../types.js";
|
|
7
|
+
import {
|
|
8
|
+
MAX_NATIVE_BYTES, MAX_NATIVE_RECORDS, NativeInputError, nativeByteLimit, object, readBoundedText,
|
|
9
|
+
reportNativeError, safeNativeId, type NativeDiscoveryOptions, type NativeReadOptions, type NativeTranscript,
|
|
10
|
+
} from "./common.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Kimi CLI keeps one directory per session under
|
|
14
|
+
* `~/.kimi/sessions/<workspace-md5>/<session-uuid>/`. `wire.jsonl` is the
|
|
15
|
+
* transcript source of truth: an append-only event stream
|
|
16
|
+
* (`{timestamp, message:{type, payload}}`, protocol stamp on line 1) whose user
|
|
17
|
+
* turns are `TurnBegin`, whose prose is `ContentPart`, whose tool calls and
|
|
18
|
+
* results are `ToolCall` / `ToolResult` rows keyed by call id, and whose
|
|
19
|
+
* lifecycle rows (`StepBegin`, `StatusUpdate`, …) are state, not conversation.
|
|
20
|
+
* A session that spawned children holds them under `subagents/<agent-id>/`,
|
|
21
|
+
* each with its own `wire.jsonl` and a `meta.json`; the parent's own
|
|
22
|
+
* `SubagentEvent` rows mirror those children's streams. `state.json` is a
|
|
23
|
+
* metadata sidecar (title) and is absent on interrupted sessions;
|
|
24
|
+
* `context.jsonl` is compacted working memory, never a source.
|
|
25
|
+
* Format facts: docs/research/2026-09-16-c10-kimi.md.
|
|
26
|
+
*/
|
|
27
|
+
const KIMI_SESSION_FILE = "wire.jsonl";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The pinned library has no Kimi adapter, so this reader projects the replayed
|
|
31
|
+
* wire into the Copilot CLI event contract — the documented event-per-line
|
|
32
|
+
* contract that keeps what a canonical record needs: a native per-row id, a
|
|
33
|
+
* per-event timestamp, tool calls linked to their results by call id, and a
|
|
34
|
+
* terminal outcome flag. `source_type` is restored to this source in the
|
|
35
|
+
* normalizer, so the projection stays a detail of a source the library cannot
|
|
36
|
+
* read.
|
|
37
|
+
*/
|
|
38
|
+
export const KIMI_PROJECTION_SOURCE = "copilot-cli";
|
|
39
|
+
|
|
40
|
+
const STATE_FILE = "state.json";
|
|
41
|
+
const META_FILE = "meta.json";
|
|
42
|
+
const REGISTRY_DIRECTORY = "subagents";
|
|
43
|
+
/** `~/.kimi/kimi.json` registers the workspace paths whose MD5 names a sessions subdirectory. */
|
|
44
|
+
const WORKSPACES_FILE = "kimi.json";
|
|
45
|
+
// <root>/<workspace>/<session>/wire.jsonl is the documented layout, and a child
|
|
46
|
+
// adds <session>/subagents/<agent>/wire.jsonl; the spare level holds a listing
|
|
47
|
+
// rooted deeper, without turning discovery into an unbounded walk.
|
|
48
|
+
const MAX_DEPTH = 4;
|
|
49
|
+
const MAX_SIDECAR_BYTES = 4 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wire events Kimi writes beside the conversation: step, stream, turn boundary,
|
|
53
|
+
* and context state.
|
|
54
|
+
*/
|
|
55
|
+
const LIFECYCLE_KINDS: Record<string, true> = {
|
|
56
|
+
StepBegin: true,
|
|
57
|
+
StepInterrupted: true,
|
|
58
|
+
ToolCallPart: true,
|
|
59
|
+
StatusUpdate: true,
|
|
60
|
+
TurnEnd: true,
|
|
61
|
+
ApprovalRequest: true,
|
|
62
|
+
ApprovalResponse: true,
|
|
63
|
+
CompactionBegin: true,
|
|
64
|
+
CompactionEnd: true,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function kimiRoots(): string[] {
|
|
68
|
+
return [join(homedir(), ".kimi", "sessions")];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface KimiState {
|
|
72
|
+
title?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nonblank(value: unknown): string | undefined {
|
|
76
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The sidecar is metadata, not transcript: a missing, oversized, or malformed
|
|
81
|
+
* state costs the title and never the session, so every failure here degrades
|
|
82
|
+
* instead of throwing.
|
|
83
|
+
*/
|
|
84
|
+
function readState(path: string): { state: KimiState; unreadable: boolean } {
|
|
85
|
+
if (!existsSync(path)) return { state: {}, unreadable: false };
|
|
86
|
+
try {
|
|
87
|
+
const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
|
|
88
|
+
if (!object(value)) return { state: {}, unreadable: true };
|
|
89
|
+
return { state: { title: nonblank(value.custom_title) }, unreadable: false };
|
|
90
|
+
} catch {
|
|
91
|
+
return { state: {}, unreadable: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A child agent's own directory records what the parent asked it to be:
|
|
97
|
+
* `subagent_type` and the launch `description`. Unreadable or absent, it costs
|
|
98
|
+
* the child's title and nickname, never the child.
|
|
99
|
+
*/
|
|
100
|
+
export function readKimiSubagentMeta(directory: string): { agentType?: string; description?: string } {
|
|
101
|
+
const path = join(directory, META_FILE);
|
|
102
|
+
if (!existsSync(path)) return {};
|
|
103
|
+
try {
|
|
104
|
+
const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
|
|
105
|
+
if (!object(value)) return {};
|
|
106
|
+
return { agentType: nonblank(value.subagent_type), description: nonblank(value.description) };
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The session's workspace path is not written in the session: the directory
|
|
114
|
+
* that holds it is the MD5 of the path Kimi was launched in, and
|
|
115
|
+
* `kimi.json` (beside the sessions root) registers those paths. Hashing each
|
|
116
|
+
* registered path finds the one this session was made from; a workspace missing
|
|
117
|
+
* from the registry (deleted, renamed, or never registered) leaves the cwd
|
|
118
|
+
* unknown instead of guessing, and the shared registry is never fatal.
|
|
119
|
+
*/
|
|
120
|
+
function readWorkspaceCwd(sessionPath: string): string | undefined {
|
|
121
|
+
const sessionDirectory = dirname(sessionPath);
|
|
122
|
+
// A child's directory is <session>/subagents/<agent>/; its workspace is the session's.
|
|
123
|
+
const owner = basename(dirname(sessionDirectory)) === REGISTRY_DIRECTORY
|
|
124
|
+
? dirname(dirname(dirname(sessionDirectory)))
|
|
125
|
+
: dirname(sessionDirectory);
|
|
126
|
+
const workspace = basename(owner);
|
|
127
|
+
if (!/^[0-9a-f]{32}$/.test(workspace)) return undefined;
|
|
128
|
+
// The registry sits beside the root that holds the workspace directories.
|
|
129
|
+
const registry = join(dirname(dirname(owner)), WORKSPACES_FILE);
|
|
130
|
+
if (!existsSync(registry)) return undefined;
|
|
131
|
+
try {
|
|
132
|
+
const value: unknown = JSON.parse(readBoundedText(registry, MAX_SIDECAR_BYTES));
|
|
133
|
+
if (!object(value) || !Array.isArray(value.work_dirs)) return undefined;
|
|
134
|
+
for (const entry of value.work_dirs) {
|
|
135
|
+
if (!object(entry)) continue;
|
|
136
|
+
const path = nonblank(entry.path);
|
|
137
|
+
if (path && createHash("md5").update(path).digest("hex") === workspace) return path;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// A registry that cannot be read costs the cwd, not the transcript.
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function discoverKimiSessions(roots: string[], options: NativeDiscoveryOptions): DiscoveredSession[] {
|
|
146
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1) throw new RangeError("Kimi discovery limit must be positive.");
|
|
147
|
+
const items: DiscoveredSession[] = [];
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
|
|
150
|
+
function walk(path: string, depth: number): void {
|
|
151
|
+
if (seen.has(path)) return;
|
|
152
|
+
seen.add(path);
|
|
153
|
+
try {
|
|
154
|
+
const stat = lstatSync(path);
|
|
155
|
+
if (stat.isSymbolicLink() || stat.isFile()) return;
|
|
156
|
+
if (depth > MAX_DEPTH) {
|
|
157
|
+
options.onDiagnostic?.({ code: "incomplete_evidence", sourcePath: path, message: "Kimi discovery reached its directory depth limit." });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const entries = readdirSync(path, { withFileTypes: true });
|
|
161
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
162
|
+
if (names.has(KIMI_SESSION_FILE)) {
|
|
163
|
+
// A directory reached through a `subagents/` registry is a child: the
|
|
164
|
+
// walk only gets there when its parent has no wire to list it (a partial
|
|
165
|
+
// rsync, a deleted transcript) or when the scan is rooted inside the
|
|
166
|
+
// registry. It is listed with the parent its path names, exactly as
|
|
167
|
+
// `collectChildren` lists it, so one transcript keeps one ledger id while
|
|
168
|
+
// the parent's wire comes and goes.
|
|
169
|
+
const parent = basename(dirname(path)) === REGISTRY_DIRECTORY ? nonblank(basename(dirname(dirname(path)))) : undefined;
|
|
170
|
+
const listing = sessionListing(path, options, parent ? { id: parent } : undefined);
|
|
171
|
+
if (!listing) return;
|
|
172
|
+
items.push(listing);
|
|
173
|
+
// A child's transcript is a session of its own, registered under the
|
|
174
|
+
// parent that spawned it. Discovery does not descend into a session
|
|
175
|
+
// otherwise: its other subdirectories hold that session's own files.
|
|
176
|
+
if (names.has(REGISTRY_DIRECTORY)) collectChildren(path, listing.id, options, items);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (entry.isDirectory()) walk(join(path, entry.name), depth + 1);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (object(error) && error.code === "ENOENT" && depth === 0) return;
|
|
184
|
+
reportNativeError(options, path);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const root of roots) walk(resolve(root), 0);
|
|
189
|
+
|
|
190
|
+
const ordered = items
|
|
191
|
+
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "") || a.path.localeCompare(b.path));
|
|
192
|
+
const nativeIds = new Set<string>();
|
|
193
|
+
const deduplicated = ordered.filter((item) => {
|
|
194
|
+
if (nativeIds.has(item.id)) return false;
|
|
195
|
+
nativeIds.add(item.id);
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
return options.select ? options.select(deduplicated) : deduplicated.slice(0, options.limit);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sidecarSize(directory: string, name: string): number {
|
|
202
|
+
try {
|
|
203
|
+
return statSync(join(directory, name)).size;
|
|
204
|
+
} catch {
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* One wire-bearing directory as a listing. A session's own directory keys it by
|
|
211
|
+
* its name; a child is keyed by the parent its path names
|
|
212
|
+
* (`<parent-uuid>:<agent-id>`) because an agent id is unique only within its
|
|
213
|
+
* parent. Every file a listing's snapshot covers is counted here — the wire and
|
|
214
|
+
* the sidecars beside it, present or not — so two callers cannot disagree about
|
|
215
|
+
* a session's size, and the title comes from `state.json` when it exists and the
|
|
216
|
+
* caller's own metadata (a child's `meta.json` description) otherwise.
|
|
217
|
+
*/
|
|
218
|
+
function sessionListing(
|
|
219
|
+
directory: string,
|
|
220
|
+
options: NativeDiscoveryOptions,
|
|
221
|
+
parent?: { id: string; toolCallId?: string; title?: string },
|
|
222
|
+
): DiscoveredSession | null {
|
|
223
|
+
const id = parent ? `${parent.id}:${basename(directory)}` : basename(directory);
|
|
224
|
+
if (!safeNativeId(id)) {
|
|
225
|
+
options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: directory, message: "Kimi session has an invalid native session ID." });
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const wire = statSync(join(directory, KIMI_SESSION_FILE));
|
|
230
|
+
const title = readState(join(directory, STATE_FILE)).state.title ?? parent?.title;
|
|
231
|
+
return {
|
|
232
|
+
id,
|
|
233
|
+
source: "kimi",
|
|
234
|
+
path: join(directory, KIMI_SESSION_FILE),
|
|
235
|
+
updatedAt: wire.mtime.toISOString(),
|
|
236
|
+
sizeBytes: wire.size + sidecarSize(directory, STATE_FILE) + sidecarSize(directory, META_FILE),
|
|
237
|
+
...(title ? { title } : {}),
|
|
238
|
+
native: {
|
|
239
|
+
kind: "kimi-wire",
|
|
240
|
+
...(parent ? { parentSessionId: parent.id } : {}),
|
|
241
|
+
...(parent?.toolCallId ? { parentToolUseId: parent.toolCallId } : {}),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (object(error) && error.code === "ENOENT") return null;
|
|
246
|
+
reportNativeError(options, directory);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A session's children are sessions: each `<parent>/subagents/<agent>/wire.jsonl`
|
|
253
|
+
* is listed with its own transcript, its parent from the path it is nested in
|
|
254
|
+
* (`kimi:<parent-uuid>:<agent-id>` — an agent id is short and only unique
|
|
255
|
+
* within its parent), and the tool call that spawned it when the parent's wire
|
|
256
|
+
* binds the two. The parent's `SubagentEvent` rows carry
|
|
257
|
+
* `parent_tool_call_id` → `agent_id`; that binding is read here, once per
|
|
258
|
+
* parent, so ingest never re-walks the store.
|
|
259
|
+
*/
|
|
260
|
+
function collectChildren(
|
|
261
|
+
parentDirectory: string,
|
|
262
|
+
parentId: string,
|
|
263
|
+
options: NativeDiscoveryOptions,
|
|
264
|
+
items: DiscoveredSession[],
|
|
265
|
+
): void {
|
|
266
|
+
const registry = join(parentDirectory, REGISTRY_DIRECTORY);
|
|
267
|
+
let entries: string[];
|
|
268
|
+
try {
|
|
269
|
+
entries = readdirSync(registry);
|
|
270
|
+
} catch {
|
|
271
|
+
reportNativeError(options, registry);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const bindings = readSpawnBindings(join(parentDirectory, KIMI_SESSION_FILE), new Set(entries), options);
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (!safeNativeId(entry)) continue;
|
|
277
|
+
const directory = join(registry, entry);
|
|
278
|
+
try {
|
|
279
|
+
// A registry entry is read only where it really is a directory: the store
|
|
280
|
+
// walk does not follow symlinks, and neither does this.
|
|
281
|
+
const stat = lstatSync(directory);
|
|
282
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
283
|
+
const listing = sessionListing(directory, options, {
|
|
284
|
+
id: parentId,
|
|
285
|
+
toolCallId: bindings.get(entry),
|
|
286
|
+
title: readKimiSubagentMeta(directory).description,
|
|
287
|
+
});
|
|
288
|
+
if (listing) items.push(listing);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (object(error) && error.code === "ENOENT") continue;
|
|
291
|
+
reportNativeError(options, directory);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The parent-side binding of a spawned child to the tool call that spawned it:
|
|
298
|
+
* `SubagentEvent` rows name both `agent_id` and `parent_tool_call_id`, and the
|
|
299
|
+
* first binding for a registered child wins. Nothing else in the store carries
|
|
300
|
+
* this edge, so it is read while the store is walked; a wire too large to read
|
|
301
|
+
* or a child bound later costs the binding, not the child's parent.
|
|
302
|
+
*/
|
|
303
|
+
function readSpawnBindings(
|
|
304
|
+
wirePath: string,
|
|
305
|
+
agents: Set<string>,
|
|
306
|
+
options: NativeDiscoveryOptions,
|
|
307
|
+
): Map<string, string> {
|
|
308
|
+
const bindings = new Map<string, string>();
|
|
309
|
+
if (agents.size === 0) return bindings;
|
|
310
|
+
let text: string;
|
|
311
|
+
try {
|
|
312
|
+
text = readBoundedText(wirePath, MAX_NATIVE_BYTES);
|
|
313
|
+
} catch {
|
|
314
|
+
options.onDiagnostic?.({
|
|
315
|
+
code: "incomplete_evidence", sourcePath: wirePath,
|
|
316
|
+
message: "Kimi subagent spawn bindings were not read: the parent's wire is unreadable or over the byte limit.",
|
|
317
|
+
});
|
|
318
|
+
return bindings;
|
|
319
|
+
}
|
|
320
|
+
for (const line of text.split("\n")) {
|
|
321
|
+
if (bindings.size === agents.size) break;
|
|
322
|
+
if (!line.includes("SubagentEvent")) continue;
|
|
323
|
+
let parsed: unknown;
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(line);
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const message = object(parsed) && object(parsed.message) ? parsed.message : null;
|
|
330
|
+
const payload = message && message.type === "SubagentEvent" && object(message.payload) ? message.payload : null;
|
|
331
|
+
if (!payload) continue;
|
|
332
|
+
const agentId = nonblank(payload.agent_id);
|
|
333
|
+
const toolCallId = nonblank(payload.parent_tool_call_id);
|
|
334
|
+
if (!agentId || !toolCallId || !agents.has(agentId) || !safeNativeId(toolCallId)) continue;
|
|
335
|
+
if (!bindings.has(agentId)) bindings.set(agentId, toolCallId);
|
|
336
|
+
}
|
|
337
|
+
return bindings;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
type Row = Record<string, unknown>;
|
|
341
|
+
|
|
342
|
+
interface WireEvent {
|
|
343
|
+
/** 1-based line number: an append-only file only ever grows at its end. */
|
|
344
|
+
line: number;
|
|
345
|
+
timestamp: string | null;
|
|
346
|
+
kind: string;
|
|
347
|
+
payload: Record<string, unknown>;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface Skips {
|
|
351
|
+
lifecycle: Map<string, number>;
|
|
352
|
+
mirror: number;
|
|
353
|
+
content: number;
|
|
354
|
+
unknown: Map<string, number>;
|
|
355
|
+
/** Result rows for a call whose result was already projected. */
|
|
356
|
+
repeats: number;
|
|
357
|
+
invalidLines: number;
|
|
358
|
+
malformedLines: number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isoSeconds(value: unknown): string | null {
|
|
362
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
|
363
|
+
return new Date(value * 1000).toISOString();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Decode the wire's own rows. The protocol stamp on line 1
|
|
368
|
+
* (`{"type":"metadata","protocol_version":"1.10"}`) is the file's header, not an
|
|
369
|
+
* event: it is neither projected nor counted as a skip. Kimi appends while it
|
|
370
|
+
* runs, so a torn last line or a line from a newer protocol is counted, never
|
|
371
|
+
* fatal: the transcript is what the prefix holds.
|
|
372
|
+
*/
|
|
373
|
+
function parseWire(text: string, skips: Skips): WireEvent[] {
|
|
374
|
+
const events: WireEvent[] = [];
|
|
375
|
+
const lines = text.split("\n");
|
|
376
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
377
|
+
const line = lines[index];
|
|
378
|
+
if (!line || !line.trim()) continue;
|
|
379
|
+
let parsed: unknown;
|
|
380
|
+
try {
|
|
381
|
+
parsed = JSON.parse(line);
|
|
382
|
+
} catch {
|
|
383
|
+
skips.invalidLines += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!object(parsed)) {
|
|
387
|
+
skips.malformedLines += 1;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (parsed.type === "metadata") continue;
|
|
391
|
+
const message = object(parsed.message) ? parsed.message : null;
|
|
392
|
+
const kind = message ? nonblank(message.type) : undefined;
|
|
393
|
+
if (!message || !kind) {
|
|
394
|
+
skips.malformedLines += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
events.push({
|
|
398
|
+
line: index + 1,
|
|
399
|
+
timestamp: isoSeconds(parsed.timestamp),
|
|
400
|
+
kind,
|
|
401
|
+
payload: object(message.payload) ? message.payload : {},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return events;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function asJsonObject(text: string): Record<string, unknown> | null {
|
|
408
|
+
if (!text.trim()) return null;
|
|
409
|
+
try {
|
|
410
|
+
const parsed: unknown = JSON.parse(text);
|
|
411
|
+
return object(parsed) ? parsed : null;
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A tool call's arguments as one JSON object. Kimi announces a call with the
|
|
419
|
+
* arguments known at that moment, so the row usually holds only a *prefix* of
|
|
420
|
+
* them (220 of 309 calls on this host open an object they do not close): the
|
|
421
|
+
* remainder streams into `ToolCallPart` rows that follow, interleaved with other
|
|
422
|
+
* events. The call's arguments are therefore the *shortest prefix* of its own
|
|
423
|
+
* argument bytes — the row's text plus the fragments up to the next call — that
|
|
424
|
+
* parses as an object.
|
|
425
|
+
*
|
|
426
|
+
* Fragments carry no call id, so they are attributed *positionally*: a fragment
|
|
427
|
+
* that follows another call's row belongs to that later call's window. The rule
|
|
428
|
+
* is exact while the protocol keeps a call's fragments ahead of the next
|
|
429
|
+
* announcement — 309/309 real calls here, none starved — but a straggler that
|
|
430
|
+
* completed an earlier call after a later one was announced would be attributed
|
|
431
|
+
* to the later call, and if the join parsed, that call would carry a
|
|
432
|
+
* wrong-but-valid object. Nothing in the wire can tell the two apart; the
|
|
433
|
+
* contract is the position. A call whose window never completes keeps the prefix
|
|
434
|
+
* the row holds, and its `tool_arguments_json` stays verbatim rather than
|
|
435
|
+
* invented.
|
|
436
|
+
*/
|
|
437
|
+
function resolveArguments(events: WireEvent[], index: number, raw: unknown): unknown {
|
|
438
|
+
const text = typeof raw === "string" ? raw : "";
|
|
439
|
+
const announced = asJsonObject(text);
|
|
440
|
+
if (announced) return announced;
|
|
441
|
+
let joined = text;
|
|
442
|
+
for (let next = index + 1; next < events.length && events[next].kind !== "ToolCall"; next += 1) {
|
|
443
|
+
if (events[next].kind !== "ToolCallPart") continue;
|
|
444
|
+
const part = events[next].payload.arguments_part;
|
|
445
|
+
if (typeof part !== "string" || !part) continue;
|
|
446
|
+
joined += part;
|
|
447
|
+
const parsed = asJsonObject(joined);
|
|
448
|
+
if (parsed) return parsed;
|
|
449
|
+
}
|
|
450
|
+
return text || {};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** A tool result's text: its output, or the message beside it when the output is empty. */
|
|
454
|
+
function resultText(returned: Record<string, unknown>): string {
|
|
455
|
+
const output = typeof returned.output === "string" ? returned.output : null;
|
|
456
|
+
if (output && output.trim()) return output;
|
|
457
|
+
const message = typeof returned.message === "string" ? returned.message : null;
|
|
458
|
+
if (message && message.trim()) return message;
|
|
459
|
+
if (output) return output;
|
|
460
|
+
if (returned.output === undefined || returned.output === null) return message ?? "";
|
|
461
|
+
return typeof returned.output === "string" ? returned.output : JSON.stringify(returned.output);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Replay the wire into the projection lane, in file order.
|
|
466
|
+
*
|
|
467
|
+
* `TurnBegin` is a user turn, `ContentPart` is one complete part (a `think` part
|
|
468
|
+
* is reasoning, a `text` part is assistant prose) — unlike Grok's token
|
|
469
|
+
* fragments, these are whole blocks, so nothing is concatenated across rows —
|
|
470
|
+
* `ToolCall` is the call, and `ToolResult` is its outcome keyed by call id with
|
|
471
|
+
* `ok = !return_value.is_error`. `SubagentEvent` rows mirror a child's stream
|
|
472
|
+
* that the child's own `wire.jsonl` holds, so they are state here; lifecycle
|
|
473
|
+
* rows are state too. Every skip is typed and counted.
|
|
474
|
+
*/
|
|
475
|
+
function replayWire(text: string, sessionId: string, cwd: string | undefined, limit: number): {
|
|
476
|
+
rows: Row[]; skips: Skips; first: string | null;
|
|
477
|
+
} {
|
|
478
|
+
const skips: Skips = {
|
|
479
|
+
lifecycle: new Map(), mirror: 0, content: 0, unknown: new Map(), repeats: 0, invalidLines: 0, malformedLines: 0,
|
|
480
|
+
};
|
|
481
|
+
const events = parseWire(text, skips);
|
|
482
|
+
const rows: Row[] = [];
|
|
483
|
+
// One result per call id: a re-emitted result row (a resumed or retried call)
|
|
484
|
+
// must not put two records behind one call.
|
|
485
|
+
const results = new Set<string>();
|
|
486
|
+
let written = 0;
|
|
487
|
+
let first: string | null = null;
|
|
488
|
+
|
|
489
|
+
function emit(row: Row): void {
|
|
490
|
+
written += Buffer.byteLength(JSON.stringify(row)) + 1;
|
|
491
|
+
if (written > limit) throw new NativeInputError("Kimi transcript exceeds the native byte limit.");
|
|
492
|
+
rows.push(row);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
496
|
+
const event = events[index];
|
|
497
|
+
// A row without a wire id keeps a stable identity from its own line: the
|
|
498
|
+
// file is append-only, so a line number never moves.
|
|
499
|
+
const id = `kimi-line-${event.line}`;
|
|
500
|
+
const at = event.timestamp ? { timestamp: event.timestamp } : {};
|
|
501
|
+
first ??= event.timestamp;
|
|
502
|
+
if (event.kind === "TurnBegin") {
|
|
503
|
+
// A top-level turn carries content parts; a subagent's turn carries its
|
|
504
|
+
// whole prompt as one string (5/5 children on this host), git context and all.
|
|
505
|
+
const input = event.payload.user_input;
|
|
506
|
+
const parts = typeof input === "string"
|
|
507
|
+
? [input]
|
|
508
|
+
: Array.isArray(input)
|
|
509
|
+
? input
|
|
510
|
+
.map((part) => (object(part) && part.type === "text" && typeof part.text === "string" ? part.text : null))
|
|
511
|
+
.filter((part): part is string => part !== null && part.length > 0)
|
|
512
|
+
: [];
|
|
513
|
+
if (!parts.length || !parts.some((part) => part.trim().length > 0)) {
|
|
514
|
+
skips.content += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
emit({
|
|
518
|
+
type: "hook.start", id, ...at,
|
|
519
|
+
data: {
|
|
520
|
+
hookType: "userPromptSubmitted",
|
|
521
|
+
input: { prompt: parts.join("\n"), sessionId, ...(cwd ? { cwd } : {}) },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (event.kind === "ContentPart") {
|
|
527
|
+
const partType = event.payload.type;
|
|
528
|
+
const raw = partType === "think" ? event.payload.think : partType === "text" ? event.payload.text : null;
|
|
529
|
+
const part = typeof raw === "string" ? raw : null;
|
|
530
|
+
if (!part || !part.trim()) {
|
|
531
|
+
skips.content += 1;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
emit({
|
|
535
|
+
type: "assistant.message", id, ...at,
|
|
536
|
+
data: partType === "think" ? { reasoningText: part } : { content: part },
|
|
537
|
+
});
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (event.kind === "ToolCall") {
|
|
541
|
+
const callId = event.payload.id;
|
|
542
|
+
if (!safeNativeId(callId)) {
|
|
543
|
+
skips.unknown.set("ToolCall_without_id", (skips.unknown.get("ToolCall_without_id") ?? 0) + 1);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const fn = object(event.payload.function) ? event.payload.function : {};
|
|
547
|
+
const name = nonblank(fn.name);
|
|
548
|
+
emit({
|
|
549
|
+
type: "assistant.message", id, ...at,
|
|
550
|
+
data: {
|
|
551
|
+
toolRequests: [{
|
|
552
|
+
toolCallId: callId,
|
|
553
|
+
...(name ? { name } : {}),
|
|
554
|
+
arguments: resolveArguments(events, index, fn.arguments),
|
|
555
|
+
}],
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (event.kind === "ToolResult") {
|
|
561
|
+
const callId = event.payload.tool_call_id;
|
|
562
|
+
if (!safeNativeId(callId)) {
|
|
563
|
+
skips.unknown.set("ToolResult_without_id", (skips.unknown.get("ToolResult_without_id") ?? 0) + 1);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (results.has(callId)) {
|
|
567
|
+
skips.repeats += 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
results.add(callId);
|
|
571
|
+
const returned = object(event.payload.return_value) ? event.payload.return_value : {};
|
|
572
|
+
const ok = returned.is_error !== true;
|
|
573
|
+
const output = resultText(returned);
|
|
574
|
+
emit({
|
|
575
|
+
type: "tool.execution_complete", id, ...at,
|
|
576
|
+
data: {
|
|
577
|
+
toolCallId: callId,
|
|
578
|
+
success: ok,
|
|
579
|
+
...(ok ? { result: { content: output } } : { error: { message: output } }),
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (event.kind === "SubagentEvent") {
|
|
585
|
+
skips.mirror += 1;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (LIFECYCLE_KINDS[event.kind] === true) {
|
|
589
|
+
skips.lifecycle.set(event.kind, (skips.lifecycle.get(event.kind) ?? 0) + 1);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
skips.unknown.set(event.kind, (skips.unknown.get(event.kind) ?? 0) + 1);
|
|
593
|
+
}
|
|
594
|
+
return { rows, skips, first };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function tally(values: Map<string, number>): number {
|
|
598
|
+
let total = 0;
|
|
599
|
+
for (const count of values.values()) total += count;
|
|
600
|
+
return total;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function named(values: Map<string, number>, limit = 4): string {
|
|
604
|
+
return [...values.keys()].sort().slice(0, limit).join(", ");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Every skip is typed and counted, so a dropped wire event never vanishes silently. */
|
|
608
|
+
function skipDiagnostics(skips: Skips): Diagnostic[] {
|
|
609
|
+
const diagnostics: Diagnostic[] = [];
|
|
610
|
+
if (skips.lifecycle.size) {
|
|
611
|
+
diagnostics.push({
|
|
612
|
+
code: "noise_record_dropped",
|
|
613
|
+
message: `Skipped ${tally(skips.lifecycle)} Kimi lifecycle wire event(s) (${named(skips.lifecycle)}).`,
|
|
614
|
+
count: tally(skips.lifecycle),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (skips.mirror) {
|
|
618
|
+
diagnostics.push({
|
|
619
|
+
code: "noise_record_dropped",
|
|
620
|
+
message: `Skipped ${skips.mirror} Kimi subagent event mirror(s); the child session's own wire holds that transcript.`,
|
|
621
|
+
count: skips.mirror,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (skips.unknown.size) {
|
|
625
|
+
diagnostics.push({
|
|
626
|
+
code: "noise_record_dropped",
|
|
627
|
+
message: `Skipped ${tally(skips.unknown)} Kimi wire event(s) with no supported transcript shape (${named(skips.unknown)}).`,
|
|
628
|
+
count: tally(skips.unknown),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (skips.repeats) {
|
|
632
|
+
diagnostics.push({
|
|
633
|
+
code: "noise_record_dropped",
|
|
634
|
+
message: `Skipped ${skips.repeats} repeated Kimi tool result row(s); the call's first result is its outcome.`,
|
|
635
|
+
count: skips.repeats,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (skips.content) {
|
|
639
|
+
diagnostics.push({
|
|
640
|
+
code: "noise_record_dropped",
|
|
641
|
+
message: `Skipped ${skips.content} Kimi message event(s) with no text part.`,
|
|
642
|
+
count: skips.content,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (skips.invalidLines) {
|
|
646
|
+
diagnostics.push({
|
|
647
|
+
code: "invalid_json_line",
|
|
648
|
+
message: `Skipped ${skips.invalidLines} invalid JSON line(s) in the Kimi wire protocol.`,
|
|
649
|
+
count: skips.invalidLines,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (skips.malformedLines) {
|
|
653
|
+
diagnostics.push({
|
|
654
|
+
code: "invalid_json_line",
|
|
655
|
+
message: `Skipped ${skips.malformedLines} Kimi wire line(s) with no event object.`,
|
|
656
|
+
count: skips.malformedLines,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return diagnostics;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function readKimiSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
|
|
663
|
+
if (!safeNativeId(item.id)) throw new NativeInputError("Unsupported Kimi native session ID.");
|
|
664
|
+
if (item.native && item.native.kind !== "kimi-wire") throw new NativeInputError("Unsupported Kimi native session.");
|
|
665
|
+
const limit = nativeByteLimit(options);
|
|
666
|
+
const text = readBoundedText(item.path, limit);
|
|
667
|
+
const directory = dirname(item.path);
|
|
668
|
+
const { state, unreadable } = readState(join(directory, STATE_FILE));
|
|
669
|
+
const cwd = readWorkspaceCwd(item.path);
|
|
670
|
+
const replay = replayWire(text, item.id, cwd, limit);
|
|
671
|
+
if (replay.rows.length > MAX_NATIVE_RECORDS) throw new NativeInputError("Kimi session exceeds the native record limit.");
|
|
672
|
+
|
|
673
|
+
// The listing's own id is stamped on the envelope, so the session keeps its
|
|
674
|
+
// group even when the wire holds nothing else this reader can project.
|
|
675
|
+
const rows: Row[] = [{
|
|
676
|
+
type: "session.start",
|
|
677
|
+
...(replay.first ? { timestamp: replay.first } : {}),
|
|
678
|
+
data: {
|
|
679
|
+
sessionId: item.id,
|
|
680
|
+
...(replay.first ? { startTime: replay.first } : {}),
|
|
681
|
+
context: { ...(cwd ? { cwd } : {}) },
|
|
682
|
+
},
|
|
683
|
+
}, ...replay.rows];
|
|
684
|
+
|
|
685
|
+
const diagnostics = skipDiagnostics(replay.skips);
|
|
686
|
+
if (unreadable) {
|
|
687
|
+
diagnostics.push({
|
|
688
|
+
code: "invalid_json_line",
|
|
689
|
+
message: "Ignored the Kimi session state sidecar: it is not a readable JSON object, so the title stays unset.",
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const title = state.title ?? item.title;
|
|
694
|
+
return {
|
|
695
|
+
transcript: `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`,
|
|
696
|
+
...(title ? { title } : {}),
|
|
697
|
+
...(cwd ? { cwd } : {}),
|
|
698
|
+
...(diagnostics.length ? { diagnostics } : {}),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** The files whose change invalidates a Kimi session's snapshot. */
|
|
703
|
+
export function kimiSessionFiles(item: DiscoveredSession): string[] {
|
|
704
|
+
const directory = dirname(item.path);
|
|
705
|
+
return [item.path, join(directory, STATE_FILE), join(directory, META_FILE)];
|
|
706
|
+
}
|