@100xprompt/brain 0.0.0-stage → 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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { basename, join } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, gitInfo, isDir, versionOf } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
8
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
9
|
+
import { LABEL, TOOL, TESTED_EVENT_VERSION } from "./records.ts"
|
|
10
|
+
import { reader } from "./reader.ts"
|
|
11
|
+
|
|
12
|
+
// Continues a portable session in the Copilot CLI as a new session: ~/.copilot/session-state/<uuid>/ with
|
|
13
|
+
// events.jsonl, workspace.yaml and checkpoints/index.md, which `copilot --resume <uuid>` opens.
|
|
14
|
+
//
|
|
15
|
+
// The records are the ones Copilot CLI 1.0.107 writes itself when it imports a session
|
|
16
|
+
// (`copilot sessions import`, checked on a real run): session.start, then user.message, assistant.message
|
|
17
|
+
// with toolRequests, one tool.execution_start per call and a tool.execution_complete per result, every
|
|
18
|
+
// event on one parentId chain with uuid ids. Failed results carry error.message, as Copilot writes them.
|
|
19
|
+
// Calls become Copilot's own tools (view, edit, create, bash, grep, glob) where their arguments fit.
|
|
20
|
+
// Its own resume loads such a session offline (session.list and session.resume over the headless server).
|
|
21
|
+
//
|
|
22
|
+
// The note saying where the session came from is a user.message with source "system", the SDK's provenance
|
|
23
|
+
// for internal application context, so it is not taken for the person's request. Readable reasoning stays
|
|
24
|
+
// as assistant.reasoning events (timeline records; Copilot replays only its own opaque reasoning). The
|
|
25
|
+
// session starts from the last compaction summary: Copilot's compaction events point at checkpoint files
|
|
26
|
+
// and model-call metadata that cannot be made up for a summary written elsewhere.
|
|
27
|
+
|
|
28
|
+
/** Copilot CLI versions whose session files this writer was checked against. */
|
|
29
|
+
export const TESTED_VERSIONS = ["1.0.107"]
|
|
30
|
+
|
|
31
|
+
/** The installed version ("GitHub Copilot CLI 1.0.107." → "1.0.107"), or undefined when it is missing. */
|
|
32
|
+
export function copilotVersion(bin = "copilot"): string | undefined {
|
|
33
|
+
return versionOf(bin)?.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/)?.[1]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The marker on the note's user.message; readers skip it like any internal context. */
|
|
37
|
+
export const NOTE_SOURCE = "system"
|
|
38
|
+
|
|
39
|
+
const continuedNote = (label: string, id: string, when: string) =>
|
|
40
|
+
`[This session continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
|
|
41
|
+
|
|
42
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
43
|
+
|
|
44
|
+
/** Copilot records the resolved folder (/private/tmp, not /tmp). */
|
|
45
|
+
function realCwd(cwd: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return realpathSync(cwd)
|
|
48
|
+
} catch {
|
|
49
|
+
return cwd
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A YAML scalar Copilot's parser reads back as the same string. */
|
|
54
|
+
const yamlString = (s: string) => JSON.stringify(s)
|
|
55
|
+
|
|
56
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
57
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
58
|
+
|
|
59
|
+
// Argument names other tools use for the same thing.
|
|
60
|
+
const FILE = ["file_path", "path", "filePath", "absolute_path", "target_file", "file", "filename"]
|
|
61
|
+
const OLD = ["old_string", "oldString", "old_str", "old_text"]
|
|
62
|
+
const NEW = ["new_string", "newString", "new_str", "new_text"]
|
|
63
|
+
const CONTENT = ["content", "contents", "file_text", "text"]
|
|
64
|
+
const COMMAND = ["command", "cmd", "commandLine"]
|
|
65
|
+
const PATTERN = ["pattern", "regex", "query", "glob_pattern"]
|
|
66
|
+
const DIR = ["path", "dir_path", "directory", "target_directory"]
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The call as Copilot CLI 1.0.107's own tool when its arguments fit one: view {path}, edit {path, old_str,
|
|
70
|
+
* new_str}, create {path, file_text}, bash {command}, grep / glob {pattern, paths}. Otherwise undefined, and
|
|
71
|
+
* the call keeps its own name and arguments.
|
|
72
|
+
*/
|
|
73
|
+
export function copilotTool(c: Call): { name: string; args: Record<string, unknown> } | undefined {
|
|
74
|
+
const input = c.input && typeof c.input === "object" && !Array.isArray(c.input) ? (c.input as Record<string, unknown>) : {}
|
|
75
|
+
const pick = (keys: string[]) => keys.map((k) => input[k]).find((v) => typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string"))) as string | string[] | undefined
|
|
76
|
+
const str = (keys: string[]) => {
|
|
77
|
+
const v = pick(keys)
|
|
78
|
+
return Array.isArray(v) ? v.join(" ") : v
|
|
79
|
+
}
|
|
80
|
+
const path = str(FILE)
|
|
81
|
+
switch (c.toolKind) {
|
|
82
|
+
case "read":
|
|
83
|
+
return path ? { name: "view", args: { path } } : undefined
|
|
84
|
+
case "edit": {
|
|
85
|
+
const [o, n] = [str(OLD), str(NEW)]
|
|
86
|
+
return path && o !== undefined && n !== undefined ? { name: "edit", args: { path, old_str: o, new_str: n } } : undefined
|
|
87
|
+
}
|
|
88
|
+
case "write": {
|
|
89
|
+
const text = str(CONTENT)
|
|
90
|
+
return path && text !== undefined ? { name: "create", args: { path, file_text: text } } : undefined
|
|
91
|
+
}
|
|
92
|
+
case "shell": {
|
|
93
|
+
const command = str(COMMAND)
|
|
94
|
+
return command ? { name: "bash", args: { command } } : undefined
|
|
95
|
+
}
|
|
96
|
+
case "search": {
|
|
97
|
+
const pattern = str(PATTERN)
|
|
98
|
+
const dir = str(DIR)
|
|
99
|
+
if (!pattern) return undefined
|
|
100
|
+
return { name: /glob/i.test(c.name) ? "glob" : "grep", args: { pattern, ...(dir ? { paths: dir } : {}) } }
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
type Event = { type: string; data: Record<string, unknown>; id: string; timestamp: string; parentId: string | null }
|
|
107
|
+
|
|
108
|
+
export interface EventsPlan {
|
|
109
|
+
events: Event[]
|
|
110
|
+
workspace: string
|
|
111
|
+
lost: string[]
|
|
112
|
+
/** What a read-back must find. */
|
|
113
|
+
expect: { users: number; assistants: number; calls: number }
|
|
114
|
+
title: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The events and workspace.yaml of a new Copilot CLI session `sessionId` continuing `session`. Pure. */
|
|
118
|
+
export function planEvents(session: Session, opts: { sessionId: string; cwd: string; version: string; now: Date; branch?: string; commit?: string }): EventsPlan {
|
|
119
|
+
const drops = new Map<string, number>()
|
|
120
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
121
|
+
const label = toolLabel(session.header.tool)
|
|
122
|
+
|
|
123
|
+
// Copilot has no way to replay a foreign compaction, so the session starts from the last summary.
|
|
124
|
+
let turns = session.turns
|
|
125
|
+
let lastCompaction = -1
|
|
126
|
+
turns.forEach((t, i) => {
|
|
127
|
+
if (t.kind === "compaction") lastCompaction = i
|
|
128
|
+
})
|
|
129
|
+
if (lastCompaction >= 0) {
|
|
130
|
+
const before = turns.slice(0, lastCompaction).filter((t) => t.kind !== "compaction").length
|
|
131
|
+
if (before) drop("turns before the last compaction (the summary stands in for them)", before)
|
|
132
|
+
turns = turns.slice(lastCompaction)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const events: Event[] = []
|
|
136
|
+
let clock = opts.now.getTime()
|
|
137
|
+
let parent: string | null = null
|
|
138
|
+
const push = (type: string, data: Record<string, unknown>) => {
|
|
139
|
+
const id = randomUUID()
|
|
140
|
+
events.push({ type, data, id, timestamp: new Date(clock++).toISOString(), parentId: parent })
|
|
141
|
+
parent = id
|
|
142
|
+
}
|
|
143
|
+
const startTime = opts.now.toISOString()
|
|
144
|
+
push("session.start", {
|
|
145
|
+
sessionId: opts.sessionId,
|
|
146
|
+
version: TESTED_EVENT_VERSION,
|
|
147
|
+
producer: "copilot-agent",
|
|
148
|
+
copilotVersion: opts.version,
|
|
149
|
+
startTime,
|
|
150
|
+
contextTier: null,
|
|
151
|
+
context: { cwd: opts.cwd, ...(opts.branch ? { branch: opts.branch } : {}), ...(opts.commit ? { headCommit: opts.commit } : {}) },
|
|
152
|
+
alreadyInUse: false,
|
|
153
|
+
})
|
|
154
|
+
push("user.message", { content: continuedNote(label, session.header.id, startTime.slice(0, 10)), source: NOTE_SOURCE })
|
|
155
|
+
|
|
156
|
+
const results = new Map<string, Result>()
|
|
157
|
+
const callIds = new Set<string>()
|
|
158
|
+
for (const t of turns) {
|
|
159
|
+
if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
160
|
+
if (t.kind === "tool_call") callIds.add(t.id)
|
|
161
|
+
}
|
|
162
|
+
// Call ids are Copilot's own uuids, mapped once so the call and its result agree.
|
|
163
|
+
const ids = new Map<string, string>()
|
|
164
|
+
const idFor = (id: string) => {
|
|
165
|
+
let v = ids.get(id)
|
|
166
|
+
if (!v) ids.set(id, (v = randomUUID()))
|
|
167
|
+
return v
|
|
168
|
+
}
|
|
169
|
+
const toolName = (name: string) => name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "tool"
|
|
170
|
+
const args = (input: unknown) => (input && typeof input === "object" && !Array.isArray(input) ? input : { input: input ?? null })
|
|
171
|
+
const nameOf = (c: Call) => copilotTool(c)?.name ?? toolName(c.name)
|
|
172
|
+
const argsOf = (c: Call) => copilotTool(c)?.args ?? args(c.input)
|
|
173
|
+
|
|
174
|
+
const expect = { users: 0, assistants: 0, calls: 0 }
|
|
175
|
+
// The assistant message being built: its text and calls, written once a result or a new message ends it.
|
|
176
|
+
let cur: { content: string; calls: Call[]; model?: string } | undefined
|
|
177
|
+
let reasoning: string[] = []
|
|
178
|
+
const answered = new Set<string>()
|
|
179
|
+
const flush = () => {
|
|
180
|
+
if (!cur) return
|
|
181
|
+
for (const text of reasoning) push("assistant.reasoning", { reasoningId: randomUUID(), content: text })
|
|
182
|
+
reasoning = []
|
|
183
|
+
// Copilot CLI 1.0.70 marks each request `type: "function"` and stamps the answering model on the message.
|
|
184
|
+
const toolRequests = cur.calls.map((c) => ({ name: nameOf(c), toolCallId: idFor(c.id), arguments: argsOf(c), type: "function" }))
|
|
185
|
+
push("assistant.message", { messageId: randomUUID(), ...(cur.model ? { model: cur.model } : {}), content: cur.content, ...(toolRequests.length ? { toolRequests } : {}) })
|
|
186
|
+
if (cur.content.trim()) expect.assistants++
|
|
187
|
+
for (const c of cur.calls) push("tool.execution_start", { toolCallId: idFor(c.id), toolName: nameOf(c), arguments: argsOf(c) })
|
|
188
|
+
// A call the session never answered is closed as a failure, so every request has its completion.
|
|
189
|
+
for (const c of cur.calls) {
|
|
190
|
+
if (results.has(c.id)) continue
|
|
191
|
+
push("tool.execution_complete", { toolCallId: idFor(c.id), success: false, error: { message: "No result was recorded for this call; the session moved on before it finished." } })
|
|
192
|
+
answered.add(c.id)
|
|
193
|
+
drop("tool calls without a result (closed as failures)")
|
|
194
|
+
}
|
|
195
|
+
cur = undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const t of turns) {
|
|
199
|
+
switch (t.kind) {
|
|
200
|
+
case "user": {
|
|
201
|
+
flush()
|
|
202
|
+
if (reasoning.length) drop("reasoning without a reply", reasoning.length)
|
|
203
|
+
reasoning = []
|
|
204
|
+
const attachments = (t.attachments ?? [])
|
|
205
|
+
.filter((a) => a.path)
|
|
206
|
+
.map((a) => ({ type: "file", path: a.path!, displayName: a.name ?? basename(a.path!), ...(a.mime ? { mimeType: a.mime } : {}) }))
|
|
207
|
+
if (!t.text.trim() && !attachments.length) break
|
|
208
|
+
push("user.message", { content: t.text, ...(attachments.length ? { attachments } : {}) })
|
|
209
|
+
expect.users++
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
case "assistant":
|
|
213
|
+
// Text after a tool call was written once the call's result came back: a new message.
|
|
214
|
+
if (cur?.calls.length) flush()
|
|
215
|
+
if (cur?.content.trim() && t.text.trim()) flush()
|
|
216
|
+
cur ??= { content: "", calls: [] }
|
|
217
|
+
cur.content = t.text
|
|
218
|
+
if (t.model) cur.model = t.model
|
|
219
|
+
break
|
|
220
|
+
case "tool_call":
|
|
221
|
+
if (ids.has(t.id)) {
|
|
222
|
+
drop("repeated tool call ids")
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
idFor(t.id)
|
|
226
|
+
cur ??= { content: "", calls: [] }
|
|
227
|
+
cur.calls.push(t)
|
|
228
|
+
expect.calls++
|
|
229
|
+
break
|
|
230
|
+
case "tool_result": {
|
|
231
|
+
if (!callIds.has(t.callId) || !ids.has(t.callId)) {
|
|
232
|
+
drop("tool results without a call")
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
if (answered.has(t.callId) || results.get(t.callId) !== t) {
|
|
236
|
+
drop("repeated tool results")
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
if (cur?.calls.some((c) => c.id === t.callId)) flush()
|
|
240
|
+
let output = t.output
|
|
241
|
+
if (t.fullPath) output += `\n\n[Full output: ${t.fullPath}]`
|
|
242
|
+
push(
|
|
243
|
+
"tool.execution_complete",
|
|
244
|
+
t.ok ? { toolCallId: idFor(t.callId), success: true, result: { content: output } } : { toolCallId: idFor(t.callId), success: false, error: { message: output || "The tool failed." } },
|
|
245
|
+
)
|
|
246
|
+
answered.add(t.callId)
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case "reasoning":
|
|
250
|
+
if (cur?.calls.length) flush()
|
|
251
|
+
reasoning.push(t.text)
|
|
252
|
+
break
|
|
253
|
+
case "compaction":
|
|
254
|
+
flush()
|
|
255
|
+
push("user.message", { content: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`, source: NOTE_SOURCE })
|
|
256
|
+
break
|
|
257
|
+
case "file_edit":
|
|
258
|
+
if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
|
|
259
|
+
break
|
|
260
|
+
case "subagent":
|
|
261
|
+
drop("subagent transcripts (their results stay in the parent's tool results)")
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
flush()
|
|
266
|
+
if (reasoning.length) drop("reasoning without a reply", reasoning.length)
|
|
267
|
+
|
|
268
|
+
const title = `${session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"} (from ${label})`
|
|
269
|
+
const workspace = [
|
|
270
|
+
`id: ${opts.sessionId}`,
|
|
271
|
+
`cwd: ${yamlString(opts.cwd)}`,
|
|
272
|
+
"client_name: github/cli",
|
|
273
|
+
"last_client_name: github/cli",
|
|
274
|
+
`name: ${yamlString(title)}`,
|
|
275
|
+
"user_named: true",
|
|
276
|
+
"summary_count: 0",
|
|
277
|
+
"fork_count: 0",
|
|
278
|
+
`created_at: ${startTime}`,
|
|
279
|
+
`updated_at: ${new Date(clock).toISOString()}`,
|
|
280
|
+
"",
|
|
281
|
+
].join("\n")
|
|
282
|
+
return { events, workspace, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect, title }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const CHECKPOINT_INDEX = `# Checkpoint History
|
|
286
|
+
|
|
287
|
+
Checkpoints are listed in chronological order. Checkpoint 1 is the oldest, higher numbers are more recent.
|
|
288
|
+
|
|
289
|
+
| # | Title | File |
|
|
290
|
+
|---|-------|------|
|
|
291
|
+
`
|
|
292
|
+
|
|
293
|
+
/** The Copilot CLI writer; `version` is for tests, which must not depend on an installed binary. */
|
|
294
|
+
export function copilotCliWriter(opts: { version?: string } = {}): Writer {
|
|
295
|
+
const installed = () => opts.version ?? copilotVersion()
|
|
296
|
+
return {
|
|
297
|
+
tool: TOOL,
|
|
298
|
+
label: LABEL,
|
|
299
|
+
way: "native",
|
|
300
|
+
|
|
301
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
302
|
+
const v = installed()
|
|
303
|
+
if (!v) return { ok: false, why: "the Copilot CLI is not installed (`copilot --version` did not run)" }
|
|
304
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Copilot CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
305
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Copilot resumes a session in its working directory` }
|
|
306
|
+
return { ok: true }
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
310
|
+
const cwd = realCwd(w.cwd)
|
|
311
|
+
const git = gitInfo(cwd, w.home)
|
|
312
|
+
const root = join(w.home, ".copilot", "session-state")
|
|
313
|
+
let sessionId = randomUUID()
|
|
314
|
+
while (exists(join(root, sessionId)) || exists(join(root, `${sessionId}.jsonl`))) sessionId = randomUUID()
|
|
315
|
+
const dir = join(root, sessionId)
|
|
316
|
+
const plan = planEvents(session, { sessionId, cwd, version: installed() ?? TESTED_VERSIONS[TESTED_VERSIONS.length - 1]!, now: new Date(), branch: git.gitBranch, commit: git.gitCommit })
|
|
317
|
+
const paths = { events: join(dir, "events.jsonl"), workspace: join(dir, "workspace.yaml"), index: join(dir, "checkpoints", "index.md") }
|
|
318
|
+
if (!w.dryRun) {
|
|
319
|
+
// mkdir without recursive on the session folder itself: an existing session is never touched.
|
|
320
|
+
mkdirSync(root, { recursive: true })
|
|
321
|
+
mkdirSync(dir)
|
|
322
|
+
try {
|
|
323
|
+
mkdirSync(join(dir, "checkpoints"))
|
|
324
|
+
writeFileSync(paths.events, `${plan.events.map((e) => JSON.stringify(e)).join("\n")}\n`, { flag: "wx" })
|
|
325
|
+
writeFileSync(paths.workspace, plan.workspace, { flag: "wx" })
|
|
326
|
+
writeFileSync(paths.index, CHECKPOINT_INDEX, { flag: "wx" })
|
|
327
|
+
const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: paths.events, extra: { dir } }, w.home)
|
|
328
|
+
const n = countTurns(back)
|
|
329
|
+
const pairs = unpaired(back)
|
|
330
|
+
if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
|
|
331
|
+
throw new Error(
|
|
332
|
+
`the new Copilot CLI session did not read back as written (users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
|
|
333
|
+
)
|
|
334
|
+
readFileSync(paths.workspace, "utf8")
|
|
335
|
+
} catch (e) {
|
|
336
|
+
rmSync(dir, { recursive: true, force: true })
|
|
337
|
+
throw e
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
tool: TOOL,
|
|
342
|
+
way: "native",
|
|
343
|
+
sessionId,
|
|
344
|
+
written: [paths.events, paths.workspace, paths.index],
|
|
345
|
+
open: `cd ${shellQuote(cwd)} && copilot --resume ${sessionId}`,
|
|
346
|
+
lost: plan.lost,
|
|
347
|
+
dryRun: !!w.dryRun,
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const copilotCliNativeWriter = copilotCliWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { crushReader } from "./reader.ts"
|
|
3
|
+
import { crushWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Charm's Crush: reads every project's .crush/crush.db listed in projects.json, and continues any session as
|
|
6
|
+
// a new native session in the project's store (with Crush closed, after a backup).
|
|
7
|
+
export const readers: Reader[] = [crushReader]
|
|
8
|
+
export const writers: Writer[] = [crushWriter]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { exists, gitInfo, readJson } from "../../fsutil.ts"
|
|
4
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
5
|
+
import { keepImage } from "../../session/attachments.ts"
|
|
6
|
+
import type { Attachment, Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
8
|
+
import { CONTINUED_NOTE } from "../opencode/note.ts"
|
|
9
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
10
|
+
import { LABEL, TOOL, projectsFile } from "./records.ts"
|
|
11
|
+
|
|
12
|
+
// Reads Crush sessions from every project store listed in projects.json (records.ts). Each store is read from
|
|
13
|
+
// a copy, as Crush keeps it in WAL mode. Subagent sessions are the store's sessions whose parent is this one
|
|
14
|
+
// (their id is the agent tool call's id); the "title-<id>" sessions Crush uses to name a session are skipped.
|
|
15
|
+
|
|
16
|
+
/** The stores Crush has registered: one per project folder it ran in. */
|
|
17
|
+
function stores(home: string): Array<{ cwd: string; db: string }> {
|
|
18
|
+
const list = readJson<{ projects?: Array<{ path?: string; data_dir?: string }> }>(projectsFile(home))?.projects ?? []
|
|
19
|
+
const seen = new Set<string>()
|
|
20
|
+
const out: Array<{ cwd: string; db: string }> = []
|
|
21
|
+
for (const p of list) {
|
|
22
|
+
if (typeof p.path !== "string" || typeof p.data_dir !== "string") continue
|
|
23
|
+
const db = join(p.data_dir, "crush.db")
|
|
24
|
+
if (seen.has(db) || !exists(db)) continue
|
|
25
|
+
seen.add(db)
|
|
26
|
+
out.push({ cwd: p.path, db })
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const crushReader: Reader = {
|
|
32
|
+
tool: TOOL,
|
|
33
|
+
label: LABEL,
|
|
34
|
+
async list(home) {
|
|
35
|
+
const refs: SessionRef[] = []
|
|
36
|
+
for (const { cwd, db } of stores(home)) {
|
|
37
|
+
try {
|
|
38
|
+
const rows = withSnapshot(db, (d) => d.query("SELECT id, title, updated_at FROM sessions WHERE parent_session_id IS NULL").all()) as Array<Record<string, any>>
|
|
39
|
+
for (const r of rows)
|
|
40
|
+
refs.push({
|
|
41
|
+
tool: TOOL,
|
|
42
|
+
sourceId: String(r.id),
|
|
43
|
+
path: db,
|
|
44
|
+
...(r.title ? { title: String(r.title) } : {}),
|
|
45
|
+
cwd,
|
|
46
|
+
...(iso(r.updated_at) ? { updatedAt: iso(r.updated_at)! } : {}),
|
|
47
|
+
})
|
|
48
|
+
} catch {
|
|
49
|
+
// A store that cannot be copied or opened lists nothing; read() says why for a session in it.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
53
|
+
},
|
|
54
|
+
async read(ref) {
|
|
55
|
+
if (!exists(ref.path)) throw new Error(`Crush store not found: ${ref.path}`)
|
|
56
|
+
return withSnapshot(ref.path, (d) => readCrush(d, ref.sourceId, { path: ref.path, ...(ref.cwd ? { cwd: ref.cwd } : {}) }))
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const json = (s: unknown): any => {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(String(s))
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One session from an open Crush store, with its subagent sessions two levels down. */
|
|
69
|
+
export function readCrush(d: Database, id: string, where: { path: string; cwd?: string }, depth = 0): Session {
|
|
70
|
+
const row = d.query("SELECT * FROM sessions WHERE id = ?").get(id) as Record<string, any> | null
|
|
71
|
+
if (!row) throw new Error(`session ${id} not found`)
|
|
72
|
+
const cwd = where.cwd
|
|
73
|
+
const h = header(TOOL, id, {
|
|
74
|
+
sourcePath: where.path,
|
|
75
|
+
...(row.title ? { title: String(row.title) } : {}),
|
|
76
|
+
project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
|
|
77
|
+
...(iso(row.created_at) ? { startedAt: iso(row.created_at)! } : {}),
|
|
78
|
+
...(iso(row.updated_at) ? { updatedAt: iso(row.updated_at)! } : {}),
|
|
79
|
+
...(row.parent_session_id ? { parent: { id: `${TOOL}:${row.parent_session_id}`, kind: "subagent" as const } } : {}),
|
|
80
|
+
})
|
|
81
|
+
const lost = lossCounter()
|
|
82
|
+
const turns: Turn[] = []
|
|
83
|
+
const callIds = new Set<string>()
|
|
84
|
+
const children = new Set(
|
|
85
|
+
(d.query("SELECT id FROM sessions WHERE parent_session_id = ? AND id NOT LIKE 'title-%'").all(id) as Array<{ id: string }>).map((r) => r.id),
|
|
86
|
+
)
|
|
87
|
+
// created_at has one-second resolution; Crush writes a session's messages in order, so rowid breaks ties.
|
|
88
|
+
for (const m of d.query("SELECT rowid, * FROM messages WHERE session_id = ? ORDER BY created_at, rowid").all(id) as Array<Record<string, any>>) {
|
|
89
|
+
const parts = json(m.parts)
|
|
90
|
+
if (!Array.isArray(parts)) {
|
|
91
|
+
lost.add("unreadable messages")
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const at = iso(m.created_at)
|
|
95
|
+
const of = (type: string) => parts.filter((p: any) => p?.type === type).map((p: any) => (p.data ?? {}) as Record<string, any>)
|
|
96
|
+
if (m.role === "user") {
|
|
97
|
+
const texts = of("text")
|
|
98
|
+
if (texts.some((t) => t.hidden)) {
|
|
99
|
+
// A continuation Crush (or brain) added for the model only; brain's says where the session came from.
|
|
100
|
+
const continued = String(texts[0]?.text ?? "").match(CONTINUED_NOTE)?.[1]
|
|
101
|
+
if (continued) h.parent ??= { id: continued, kind: "continued" }
|
|
102
|
+
else lost.add("messages the tool injected as the user")
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
for (const sc of of("shell_command")) {
|
|
106
|
+
const callId = `shell-${m.id}-${callIds.size}`
|
|
107
|
+
callIds.add(callId)
|
|
108
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: callId, name: "bash", toolKind: "shell", input: { command: sc.command } })
|
|
109
|
+
turns.push({ kind: "tool_result", callId, ok: !sc.exit_code, output: clip(String(sc.output ?? "")).text })
|
|
110
|
+
}
|
|
111
|
+
if (of("sub_agent_report").length) lost.add("subagent reports", of("sub_agent_report").length)
|
|
112
|
+
const attachments: Attachment[] = [
|
|
113
|
+
...of("binary").map((b) => {
|
|
114
|
+
const mime = String(b.MIMEType ?? "")
|
|
115
|
+
const kept = mime.startsWith("image/") && typeof b.Data === "string" ? keepImage(b.Data, mime) : undefined
|
|
116
|
+
if (kept) return kept
|
|
117
|
+
return { kind: (mime.startsWith("image/") ? "image" : "file") as Attachment["kind"], ...(b.Path ? { path: String(b.Path) } : {}), ...(b.MIMEType ? { mime } : {}) }
|
|
118
|
+
}),
|
|
119
|
+
...of("image_url").map(() => ({ kind: "image" as const })),
|
|
120
|
+
]
|
|
121
|
+
if (of("image_url").length) lost.add("attachment data", of("image_url").length)
|
|
122
|
+
const text = texts.map((t) => String(t.text ?? "")).join("\n\n")
|
|
123
|
+
if (text.trim() || attachments.length) turns.push({ kind: "user", ...(at ? { at } : {}), text, ...(attachments.length ? { attachments } : {}) })
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (m.role === "tool") {
|
|
127
|
+
for (const r of of("tool_result")) {
|
|
128
|
+
const callId = String(r.tool_call_id ?? "")
|
|
129
|
+
if (r.data) lost.add("images in tool results")
|
|
130
|
+
const out = clip(String(r.content ?? ""))
|
|
131
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId, ok: !r.is_error, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
|
|
132
|
+
}
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (m.role !== "assistant") {
|
|
136
|
+
lost.add(`unknown message roles: ${String(m.role)}`)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const text = of("text").map((t) => String(t.text ?? "")).join("\n\n")
|
|
140
|
+
if (m.is_summary_message) {
|
|
141
|
+
if (text.trim()) turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: text })
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
const model = m.model ? String(m.model) : undefined
|
|
145
|
+
if (model && (text.trim() || of("tool_call").length)) noteModel(h, model)
|
|
146
|
+
for (const r of of("reasoning")) {
|
|
147
|
+
const signed = !!(r.signature || r.thought_signature || r.responses_data)
|
|
148
|
+
if (typeof r.thinking === "string" && r.thinking.trim()) {
|
|
149
|
+
turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: r.thinking })
|
|
150
|
+
if (signed) lost.add("reasoning signatures")
|
|
151
|
+
} else if (signed) lost.add("signed reasoning")
|
|
152
|
+
}
|
|
153
|
+
if (text.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text, ...(model ? { model } : {}) })
|
|
154
|
+
for (const c of of("tool_call")) {
|
|
155
|
+
const name = String(c.name ?? "unknown")
|
|
156
|
+
let callId = String(c.id ?? "")
|
|
157
|
+
for (let n = 2; !callId || callIds.has(callId); n++) callId = `${String(c.id ?? "call")}#${n}`
|
|
158
|
+
callIds.add(callId)
|
|
159
|
+
const input = json(c.input) ?? c.input ?? {}
|
|
160
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: callId, name, toolKind: toolKindOf(name), input })
|
|
161
|
+
if (children.has(String(c.id))) turns.push({ kind: "subagent", ...(at ? { at } : {}), sessionId: `${TOOL}:${c.id}`, callId })
|
|
162
|
+
}
|
|
163
|
+
const finish = of("finish")[0]
|
|
164
|
+
if (finish && (finish.reason === "error" || finish.reason === "content_filter")) lost.add("error notices on replies")
|
|
165
|
+
}
|
|
166
|
+
lost.flush(h)
|
|
167
|
+
const s: Session = { header: h, turns }
|
|
168
|
+
if (depth < 2 && children.size) {
|
|
169
|
+
const kids: Session[] = []
|
|
170
|
+
for (const child of children) {
|
|
171
|
+
try {
|
|
172
|
+
kids.push(readCrush(d, child, where, depth + 1))
|
|
173
|
+
} catch {
|
|
174
|
+
h.lost.push(`unreadable subagent session (${child})`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (kids.length) s.children = kids
|
|
178
|
+
}
|
|
179
|
+
return s
|
|
180
|
+
}
|