@animalabs/connectome-host 0.3.5 → 0.3.8
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/.env.example +6 -0
- package/.github/workflows/ci.yml +52 -0
- package/.github/workflows/publish.yml +2 -2
- package/README.md +5 -0
- package/bun.lock +14 -6
- package/docs/AGENT-ONBOARDING.md +6 -1
- package/package.json +6 -4
- package/scripts/import-codex-rollout.ts +288 -0
- package/src/call-ledger.ts +371 -0
- package/src/call-pricing.ts +119 -0
- package/src/framework-agent-config.ts +51 -0
- package/src/framework-strategy.ts +70 -0
- package/src/index.ts +130 -105
- package/src/logging-adapter.ts +105 -12
- package/src/mcpl-config.ts +1 -0
- package/src/modules/channel-mode-module.ts +14 -16
- package/src/modules/fleet-module.ts +17 -7
- package/src/modules/mcpl-admin-module.ts +6 -15
- package/src/modules/settings-module.ts +83 -59
- package/src/modules/subscription-gc-module.ts +17 -20
- package/src/modules/time-module.ts +15 -9
- package/src/modules/web-ui-module.ts +88 -10
- package/src/modules/web-ui-observers.ts +35 -9
- package/src/recipe.ts +133 -6
- package/src/state/agent-tree-reducer.ts +17 -3
- package/src/strategies/frontdesk-strategy.ts +15 -9
- package/src/web/protocol.ts +89 -0
- package/test/agent-tree-reducer.test.ts +35 -3
- package/test/autobio-progress-snapshot.test.ts +27 -12
- package/test/call-ledger.test.ts +91 -0
- package/test/call-pricing.test.ts +69 -0
- package/test/framework-fkm-composition.test.ts +160 -0
- package/test/frontdesk-strategy.test.ts +10 -0
- package/test/import-codex-rollout.test.ts +36 -0
- package/test/logging-adapter-refusal.test.ts +57 -0
- package/test/logging-adapter.test.ts +58 -1
- package/test/recipe-cache-ttl.test.ts +7 -3
- package/test/recipe-compression-fallback.test.ts +36 -0
- package/test/recipe-primary-summary-fallback.test.ts +40 -0
- package/test/recipe-provider.test.ts +36 -0
- package/test/recipe-think-policy.test.ts +39 -0
- package/test/recipe-timezone.test.ts +20 -0
- package/test/subscription-gc-module.test.ts +7 -5
- package/test/time-module.test.ts +13 -0
- package/test/web-ui-observers.test.ts +70 -2
- package/web/package-lock.json +425 -302
- package/web/src/App.tsx +9 -0
- package/web/src/ObserverGate.tsx +21 -11
- package/web/src/Usage.tsx +116 -2
- package/web/src/wire.ts +5 -2
- package/web/bun.lock +0 -357
package/.env.example
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
2
2
|
# MODEL=claude-opus-4-6
|
|
3
|
+
# Agent-visible wall clock (IANA zone; recipe agent.timezone takes precedence)
|
|
4
|
+
# AGENT_TIMEZONE=America/Los_Angeles
|
|
5
|
+
|
|
6
|
+
# For recipes with agent.provider="openai-responses":
|
|
7
|
+
# OPENAI_API_KEY=sk-...
|
|
8
|
+
# OPENAI_BASE_URL=https://api.openai.com/v1
|
|
3
9
|
|
|
4
10
|
# --- Recipe ${VAR} substitutions ---------------------------------------
|
|
5
11
|
# Recipes in recipes/ reference these via ${VAR}. Unset = that branch of
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-test:
|
|
14
|
+
name: Build & Test (${{ matrix.os }})
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
# Both platforms on purpose: a package-lock.json regenerated over an
|
|
19
|
+
# existing node_modules tree records only that machine's native binaries
|
|
20
|
+
# (esbuild/rollup/tailwind-oxide), so a lock made on macOS breaks Linux
|
|
21
|
+
# installs and vice versa. Each runner catches the lock the other's
|
|
22
|
+
# platform produced. macOS runs the install+build lock guard only;
|
|
23
|
+
# the full test suite runs on ubuntu.
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, macos-latest]
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v6
|
|
29
|
+
|
|
30
|
+
- name: Setup Node.js
|
|
31
|
+
uses: actions/setup-node@v6
|
|
32
|
+
with:
|
|
33
|
+
node-version: 24
|
|
34
|
+
|
|
35
|
+
- name: Setup Bun
|
|
36
|
+
uses: oven-sh/setup-bun@v2
|
|
37
|
+
with:
|
|
38
|
+
bun-version: 1.3.14
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: npm install
|
|
42
|
+
|
|
43
|
+
# npm ci is the actual lockfile guard: `npm install` (npm >= 11)
|
|
44
|
+
# silently re-resolves platform packages missing from the lock, so a
|
|
45
|
+
# broken lock sails through it. npm ci installs the lock verbatim and
|
|
46
|
+
# the vite build then fails loudly on the missing rollup binary.
|
|
47
|
+
- name: Build web assets (strict lockfile)
|
|
48
|
+
run: npm run build:web:ci
|
|
49
|
+
|
|
50
|
+
- name: Test
|
|
51
|
+
if: matrix.os == 'ubuntu-latest'
|
|
52
|
+
run: npm test
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
run: npm install
|
|
32
32
|
|
|
33
33
|
- name: Build web assets
|
|
34
|
-
run: npm run build:web
|
|
34
|
+
run: npm run build:web:ci
|
|
35
35
|
|
|
36
36
|
- name: Test
|
|
37
37
|
run: npm test
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
run: npm install
|
|
65
65
|
|
|
66
66
|
- name: Build web assets
|
|
67
|
-
run: npm run build:web
|
|
67
|
+
run: npm run build:web:ci
|
|
68
68
|
|
|
69
69
|
- name: Publish
|
|
70
70
|
run: npm publish --access public --provenance
|
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ A recipe is a JSON file that configures everything domain-specific:
|
|
|
27
27
|
"agent": {
|
|
28
28
|
"name": "researcher",
|
|
29
29
|
"model": "claude-opus-4-6",
|
|
30
|
+
"timezone": "America/Los_Angeles",
|
|
30
31
|
"systemPrompt": "You are a ...",
|
|
31
32
|
"maxTokens": 16384,
|
|
32
33
|
"strategy": {
|
|
@@ -55,6 +56,10 @@ A recipe is a JSON file that configures everything domain-specific:
|
|
|
55
56
|
}
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
`agent.timezone` is an IANA zone used only for times rendered to the agent.
|
|
60
|
+
Chronicle and MCPL protocol timestamps remain epoch/UTC. If the recipe omits
|
|
61
|
+
it, `AGENT_TIMEZONE` is used, then the process timezone.
|
|
62
|
+
|
|
58
63
|
### Recipe loading
|
|
59
64
|
|
|
60
65
|
| Command | Behavior |
|
package/bun.lock
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "connectome-host",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@animalabs/agent-framework": "^0.6.
|
|
8
|
+
"@animalabs/agent-framework": "^0.6.7",
|
|
9
9
|
"@animalabs/chronicle": "^0.2.0",
|
|
10
|
-
"@animalabs/context-manager": "^0.5.
|
|
11
|
-
"@animalabs/membrane": "^0.5.
|
|
10
|
+
"@animalabs/context-manager": "^0.5.12",
|
|
11
|
+
"@animalabs/membrane": "^0.5.71",
|
|
12
12
|
"@opentui/core": "^0.1.82",
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
"packages": {
|
|
22
|
-
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.6.
|
|
22
|
+
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.6.7", "", { "dependencies": { "@animalabs/chronicle": "^0.2.2", "@animalabs/context-manager": "^0.5.12", "@animalabs/membrane": "^0.5.73", "chokidar": "^4.0.3", "discord.js": "^14.25.1", "ws": "^8.18.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js", "agent-framework-recover": "dist/src/recovery/recover-cli.js" } }, "sha512-I6hOhmF75AQDeyd2r0JFaR2QHyEAXiVQlr5MgsriUTcFJbtZM7n+yd4aiWbqeXGDeEHjbEpkiifn7vMeOfIeog=="],
|
|
23
23
|
|
|
24
24
|
"@animalabs/chronicle": ["@animalabs/chronicle@0.2.1", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.1", "@animalabs/chronicle-darwin-x64": "0.2.1", "@animalabs/chronicle-linux-arm64-gnu": "0.2.1", "@animalabs/chronicle-linux-x64-gnu": "0.2.1", "@animalabs/chronicle-win32-x64-msvc": "0.2.1" } }, "sha512-rDFc069yfi7BQUusgsXBwBdqljUWLByXSYaTF40AP+tonBh0No3eF9VzKTSy1I8DYg/jAUHIKtVdKDGsVMVubw=="],
|
|
25
25
|
|
|
26
|
-
"@animalabs/context-manager": ["@animalabs/context-manager@0.5.
|
|
26
|
+
"@animalabs/context-manager": ["@animalabs/context-manager@0.5.12", "", { "dependencies": { "@animalabs/chronicle": "^0.2.6", "@animalabs/membrane": "^0.5.69" } }, "sha512-V/TTnIuvFtVYu+A9UTi7A0Q8gCuMTRUatt27JuC9nSDVIQ5L2sMlnsVGbIS8rqOkB8i9o5+Rq/itH5wZVQgaVA=="],
|
|
27
27
|
|
|
28
|
-
"@animalabs/membrane": ["@animalabs/membrane@0.5.
|
|
28
|
+
"@animalabs/membrane": ["@animalabs/membrane@0.5.71", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-Jos4Fh/kIbpOiJ43LKIFpmaCmWIzE6ly1x/4c5iHaMw8n6D8OJ2f4va7xo3vNdvfKWBbln4QghmNg4NWo3oFGA=="],
|
|
29
29
|
|
|
30
30
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.52.0", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ=="],
|
|
31
31
|
|
|
@@ -275,6 +275,14 @@
|
|
|
275
275
|
|
|
276
276
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
277
277
|
|
|
278
|
+
"@animalabs/agent-framework/@animalabs/chronicle": ["@animalabs/chronicle@0.2.6", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.6", "@animalabs/chronicle-darwin-x64": "0.2.6", "@animalabs/chronicle-linux-arm64-gnu": "0.2.6", "@animalabs/chronicle-linux-x64-gnu": "0.2.6", "@animalabs/chronicle-win32-x64-msvc": "0.2.6" } }, "sha512-+gbt2GR4SP8MnKxeqkJZf6oZn339fLiBtk6GHyBD/gHQ4e4eFqBpTaTd5eKycKBws0xWGcOqyiJFH4IdBkweUA=="],
|
|
279
|
+
|
|
280
|
+
"@animalabs/agent-framework/@animalabs/membrane": ["@animalabs/membrane@0.5.73", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-w1rt/JVOD6dWwEO54tfR0yrLzwwD45cpYJ9hWJhHxs8k9FvW1PgIIV1KmNFNbpY8MR9HC5g7Lqgvhzs3pDst+g=="],
|
|
281
|
+
|
|
282
|
+
"@animalabs/context-manager/@animalabs/chronicle": ["@animalabs/chronicle@0.2.6", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.6", "@animalabs/chronicle-darwin-x64": "0.2.6", "@animalabs/chronicle-linux-arm64-gnu": "0.2.6", "@animalabs/chronicle-linux-x64-gnu": "0.2.6", "@animalabs/chronicle-win32-x64-msvc": "0.2.6" } }, "sha512-+gbt2GR4SP8MnKxeqkJZf6oZn339fLiBtk6GHyBD/gHQ4e4eFqBpTaTd5eKycKBws0xWGcOqyiJFH4IdBkweUA=="],
|
|
283
|
+
|
|
284
|
+
"@animalabs/context-manager/@animalabs/membrane": ["@animalabs/membrane@0.5.73", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-w1rt/JVOD6dWwEO54tfR0yrLzwwD45cpYJ9hWJhHxs8k9FvW1PgIIV1KmNFNbpY8MR9HC5g7Lqgvhzs3pDst+g=="],
|
|
285
|
+
|
|
278
286
|
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
|
279
287
|
|
|
280
288
|
"@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="],
|
package/docs/AGENT-ONBOARDING.md
CHANGED
|
@@ -239,11 +239,12 @@ ln -sfn ../../../connectome-local/context-manager/node_modules/@animalabs/chroni
|
|
|
239
239
|
"agent": {
|
|
240
240
|
"name": "<agent>", // = chronicle "self" participant
|
|
241
241
|
"model": "<model-id>", // or gateway-prefixed, e.g. anthropic/claude-opus-4
|
|
242
|
+
"timezone": "America/Los_Angeles", // agent-visible wall clock only; storage stays UTC
|
|
242
243
|
"systemPrompt": "<persona / or minimal>",
|
|
243
244
|
"maxTokens": 16384, // response cap
|
|
244
245
|
"maxStreamTokens": 180000, // recompile trigger; keep < model window
|
|
245
246
|
"contextBudgetTokens": 160000, // SEE §11.1 — MUST fit under (window - maxTokens)
|
|
246
|
-
"cacheTtl": "1h", // prompt-cache TTL; '1h'
|
|
247
|
+
"cacheTtl": "1h", // prompt-cache TTL; defaults to '1h', set '5m' explicitly for rapid sub-5m loops
|
|
247
248
|
"strategy": {
|
|
248
249
|
"type": "autobiographical",
|
|
249
250
|
"headWindowTokens": 4000,
|
|
@@ -266,6 +267,10 @@ ln -sfn ../../../connectome-local/context-manager/node_modules/@animalabs/chroni
|
|
|
266
267
|
}
|
|
267
268
|
```
|
|
268
269
|
|
|
270
|
+
Use `"cacheTtl": "1h"` for Connectome deployments. This is also the runtime
|
|
271
|
+
default when the field is omitted. Set `"5m"` only for intentionally rapid
|
|
272
|
+
workloads whose cache is normally reused within five minutes.
|
|
273
|
+
|
|
269
274
|
**`.env`** (`chmod 600`). Common vars:
|
|
270
275
|
|
|
271
276
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/connectome-host",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "General-purpose agent TUI host with recipe-based configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
"dev": "bun --watch src/index.ts",
|
|
9
9
|
"test": "bun test",
|
|
10
10
|
"build:web": "npm --prefix web install && npm --prefix web run build",
|
|
11
|
+
"build:web:ci": "npm ci --prefix web && npm --prefix web run build",
|
|
12
|
+
"relock:web": "rm -rf web/node_modules web/package-lock.json && npm --prefix web install",
|
|
11
13
|
"postinstall": "test -d web && npm --prefix web install && npm --prefix web run build || true"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
|
-
"@animalabs/agent-framework": "^0.6.
|
|
16
|
+
"@animalabs/agent-framework": "^0.6.7",
|
|
15
17
|
"@animalabs/chronicle": "^0.2.0",
|
|
16
|
-
"@animalabs/context-manager": "^0.5.
|
|
17
|
-
"@animalabs/membrane": "^0.5.
|
|
18
|
+
"@animalabs/context-manager": "^0.5.12",
|
|
19
|
+
"@animalabs/membrane": "^0.5.71",
|
|
18
20
|
"@opentui/core": "^0.1.82"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Import a Codex rollout JSONL as a KV-stable OpenAI Responses session. */
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
dirname,
|
|
7
|
+
join,
|
|
8
|
+
resolve,
|
|
9
|
+
} from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { JsStore } from '@animalabs/chronicle';
|
|
16
|
+
import { ContextManager } from '@animalabs/context-manager';
|
|
17
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
18
|
+
import { SessionManager } from '../src/session-manager.js';
|
|
19
|
+
|
|
20
|
+
interface RolloutRecord {
|
|
21
|
+
timestamp?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NativeItem {
|
|
27
|
+
type?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
role?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface EffectiveItem {
|
|
34
|
+
item: NativeItem;
|
|
35
|
+
timestamp?: string;
|
|
36
|
+
sourceLine: number;
|
|
37
|
+
restoredByCompaction: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function usage(): never {
|
|
41
|
+
console.error('Usage: bun scripts/import-codex-rollout.ts <rollout.jsonl> --out <instance-dir> [--agent <name>]');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv: string[]) {
|
|
46
|
+
const args = argv.slice(2);
|
|
47
|
+
if (!args[0] || args[0].startsWith('-')) usage();
|
|
48
|
+
const source = resolve(args[0]);
|
|
49
|
+
let outDir = '';
|
|
50
|
+
let agentName = 'Codex';
|
|
51
|
+
for (let i = 1; i < args.length; i++) {
|
|
52
|
+
if (args[i] === '--out') outDir = resolve(args[++i] ?? '');
|
|
53
|
+
else if (args[i] === '--agent') agentName = args[++i] ?? usage();
|
|
54
|
+
else usage();
|
|
55
|
+
}
|
|
56
|
+
if (!outDir) usage();
|
|
57
|
+
return { source, outDir, agentName };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reconstruct the provider's current input window. A `compacted` record
|
|
61
|
+
* replaces all prior response-item history with its canonical
|
|
62
|
+
* `replacement_history`; later response items append normally. */
|
|
63
|
+
export function reconstructEffectiveHistory(records: RolloutRecord[]): EffectiveItem[] {
|
|
64
|
+
let history: EffectiveItem[] = [];
|
|
65
|
+
for (const [index, record] of records.entries()) {
|
|
66
|
+
if (record.type === 'compacted') {
|
|
67
|
+
const replacement = record.payload?.replacement_history;
|
|
68
|
+
if (Array.isArray(replacement)) {
|
|
69
|
+
history = replacement.map(item => ({
|
|
70
|
+
item: item as NativeItem,
|
|
71
|
+
timestamp: record.timestamp,
|
|
72
|
+
sourceLine: index + 1,
|
|
73
|
+
restoredByCompaction: true,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (record.type === 'response_item' && record.payload) {
|
|
79
|
+
history.push({
|
|
80
|
+
item: record.payload as NativeItem,
|
|
81
|
+
timestamp: record.timestamp,
|
|
82
|
+
sourceLine: index + 1,
|
|
83
|
+
restoredByCompaction: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return history;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function textFromContent(content: unknown): string {
|
|
91
|
+
if (typeof content === 'string') return content;
|
|
92
|
+
if (!Array.isArray(content)) return '';
|
|
93
|
+
return content
|
|
94
|
+
.map(part => {
|
|
95
|
+
if (!part || typeof part !== 'object') return '';
|
|
96
|
+
const value = part as Record<string, unknown>;
|
|
97
|
+
return typeof value.text === 'string' ? value.text
|
|
98
|
+
: typeof value.refusal === 'string' ? value.refusal
|
|
99
|
+
: '';
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A human-readable Chronicle projection. `rawItem` is the load-bearing field:
|
|
106
|
+
* the Responses formatter emits it verbatim and ignores the projection. */
|
|
107
|
+
export function projectItem(item: NativeItem): ContentBlock[] {
|
|
108
|
+
const carrier = <T extends ContentBlock>(block: T): ContentBlock =>
|
|
109
|
+
({ ...block, rawItem: item } as ContentBlock);
|
|
110
|
+
|
|
111
|
+
switch (item.type) {
|
|
112
|
+
case 'message':
|
|
113
|
+
return [carrier({ type: 'text', text: textFromContent(item.content) })];
|
|
114
|
+
case 'reasoning':
|
|
115
|
+
if (typeof item.encrypted_content === 'string') {
|
|
116
|
+
return [carrier({ type: 'redacted_thinking', data: item.encrypted_content })];
|
|
117
|
+
}
|
|
118
|
+
return [carrier({ type: 'thinking', thinking: textFromContent(item.summary) })];
|
|
119
|
+
case 'compaction':
|
|
120
|
+
return [carrier({
|
|
121
|
+
type: 'redacted_thinking',
|
|
122
|
+
data: typeof item.encrypted_content === 'string' ? item.encrypted_content : '',
|
|
123
|
+
})];
|
|
124
|
+
case 'function_call':
|
|
125
|
+
return [carrier({
|
|
126
|
+
type: 'tool_use',
|
|
127
|
+
id: String(item.call_id ?? item.id ?? ''),
|
|
128
|
+
name: String(item.name ?? ''),
|
|
129
|
+
input: (() => {
|
|
130
|
+
try { return JSON.parse(String(item.arguments ?? '{}')) as Record<string, unknown>; }
|
|
131
|
+
catch { return { _rawArguments: item.arguments }; }
|
|
132
|
+
})(),
|
|
133
|
+
})];
|
|
134
|
+
case 'function_call_output':
|
|
135
|
+
return [carrier({
|
|
136
|
+
type: 'tool_result',
|
|
137
|
+
toolUseId: String(item.call_id ?? ''),
|
|
138
|
+
content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? null),
|
|
139
|
+
})];
|
|
140
|
+
default:
|
|
141
|
+
// Custom tool calls/outputs and future item types stay opaque. The
|
|
142
|
+
// zero-width projection avoids injecting synthetic text into the model.
|
|
143
|
+
return [carrier({ type: 'text', text: '' })];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function participantFor(item: NativeItem, agentName: string): string {
|
|
148
|
+
if (item.type === 'message') return item.role === 'assistant' ? agentName : 'user';
|
|
149
|
+
if (item.type === 'reasoning' || item.type === 'compaction' ||
|
|
150
|
+
item.type === 'function_call' || item.type === 'custom_tool_call') return agentName;
|
|
151
|
+
return 'user';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeRecipe(instanceDir: string, agentName: string): string {
|
|
155
|
+
const recipeDir = join(instanceDir, 'recipes');
|
|
156
|
+
mkdirSync(recipeDir, { recursive: true });
|
|
157
|
+
const path = join(recipeDir, 'codex-rollout.json');
|
|
158
|
+
const recipe = {
|
|
159
|
+
name: `${agentName} rollout revival`,
|
|
160
|
+
description: 'Stateless OpenAI Responses continuation imported from a Codex rollout.',
|
|
161
|
+
version: '1',
|
|
162
|
+
agent: {
|
|
163
|
+
name: agentName,
|
|
164
|
+
provider: 'openai-responses',
|
|
165
|
+
model: 'gpt-5.6-sol',
|
|
166
|
+
systemPrompt: 'The native imported Responses history contains the authoritative developer and system instructions.',
|
|
167
|
+
maxTokens: 128000,
|
|
168
|
+
maxStreamTokens: 900000,
|
|
169
|
+
contextBudgetTokens: 1000000,
|
|
170
|
+
strategy: { type: 'passthrough' },
|
|
171
|
+
responses: {
|
|
172
|
+
reasoningEffort: 'high',
|
|
173
|
+
reasoningContext: 'all_turns',
|
|
174
|
+
compactThreshold: 850000,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
modules: {
|
|
178
|
+
subagents: false,
|
|
179
|
+
lessons: false,
|
|
180
|
+
retrieval: false,
|
|
181
|
+
wake: false,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
writeFileSync(path, JSON.stringify(recipe, null, 2) + '\n');
|
|
185
|
+
return path;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function main() {
|
|
189
|
+
const opts = parseArgs(process.argv);
|
|
190
|
+
const raw = readFileSync(opts.source, 'utf8');
|
|
191
|
+
const records = raw.trim().split('\n').filter(Boolean).map((line, index) => {
|
|
192
|
+
try { return JSON.parse(line) as RolloutRecord; }
|
|
193
|
+
catch (error) { throw new Error(`Invalid JSON at ${opts.source}:${index + 1}: ${String(error)}`); }
|
|
194
|
+
});
|
|
195
|
+
const history = reconstructEffectiveHistory(records);
|
|
196
|
+
if (history.length === 0) throw new Error('Rollout contains no effective response items.');
|
|
197
|
+
|
|
198
|
+
const dataDir = join(opts.outDir, 'data');
|
|
199
|
+
const sourceDir = join(opts.outDir, 'source');
|
|
200
|
+
mkdirSync(sourceDir, { recursive: true });
|
|
201
|
+
const snapshotPath = join(sourceDir, basename(opts.source));
|
|
202
|
+
writeFileSync(snapshotPath, raw);
|
|
203
|
+
|
|
204
|
+
const sessionManager = new SessionManager(dataDir);
|
|
205
|
+
const session = sessionManager.createSession(`Codex ${basename(opts.source).replace(/^rollout-|\.jsonl$/g, '')}`);
|
|
206
|
+
const storePath = sessionManager.getStorePath(session.id);
|
|
207
|
+
const store = JsStore.openOrCreate({ path: storePath });
|
|
208
|
+
try {
|
|
209
|
+
const cm = await ContextManager.open({ store });
|
|
210
|
+
try {
|
|
211
|
+
for (const entry of history) {
|
|
212
|
+
cm.addMessage(
|
|
213
|
+
participantFor(entry.item, opts.agentName),
|
|
214
|
+
projectItem(entry.item),
|
|
215
|
+
{
|
|
216
|
+
sourceId: entry.item.id ?? `rollout-line-${entry.sourceLine}`,
|
|
217
|
+
codexRollout: {
|
|
218
|
+
sourceLine: entry.sourceLine,
|
|
219
|
+
sourceTimestamp: entry.timestamp,
|
|
220
|
+
restoredByCompaction: entry.restoredByCompaction,
|
|
221
|
+
nativeType: entry.item.type,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
await cm.close?.();
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
store.close?.();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const meta = records.find(record => record.type === 'session_meta')?.payload ?? {};
|
|
234
|
+
const turnContexts = records.filter(record => record.type === 'turn_context');
|
|
235
|
+
const latestTurn = turnContexts.at(-1)?.payload ?? {};
|
|
236
|
+
const compacted = records.filter(record => record.type === 'compacted');
|
|
237
|
+
const sidecarPath = join(dirname(storePath), `${session.id}.import-source.json`);
|
|
238
|
+
writeFileSync(sidecarPath, JSON.stringify({
|
|
239
|
+
source: opts.source,
|
|
240
|
+
snapshot: snapshotPath,
|
|
241
|
+
agentName: opts.agentName,
|
|
242
|
+
importedAt: new Date().toISOString(),
|
|
243
|
+
originalRecordCount: records.length,
|
|
244
|
+
importedMessageCount: history.length,
|
|
245
|
+
effectiveItemCount: history.length,
|
|
246
|
+
compactionCount: compacted.length,
|
|
247
|
+
latestWindowId: compacted.at(-1)?.payload?.window_id ?? null,
|
|
248
|
+
model: latestTurn.model ?? 'gpt-5.6-sol',
|
|
249
|
+
sessionMeta: meta,
|
|
250
|
+
}, null, 2) + '\n');
|
|
251
|
+
|
|
252
|
+
const index = sessionManager.load();
|
|
253
|
+
index.sessions[session.id]!.messageCount = history.length;
|
|
254
|
+
sessionManager.save(index);
|
|
255
|
+
const recipePath = writeRecipe(opts.outDir, opts.agentName);
|
|
256
|
+
writeFileSync(join(opts.outDir, '.env.example'), [
|
|
257
|
+
`DATA_DIR=${dataDir}`,
|
|
258
|
+
'OPENAI_API_KEY=',
|
|
259
|
+
'',
|
|
260
|
+
].join('\n'));
|
|
261
|
+
writeFileSync(join(opts.outDir, 'README.md'), [
|
|
262
|
+
'# Codex rollout Connectome instance',
|
|
263
|
+
'',
|
|
264
|
+
`Imported from \`${opts.source}\`.`,
|
|
265
|
+
'',
|
|
266
|
+
'Run from the connectome-host checkout:',
|
|
267
|
+
'',
|
|
268
|
+
'```sh',
|
|
269
|
+
`DATA_DIR=${dataDir} OPENAI_API_KEY=... bun src/index.ts ${recipePath}`,
|
|
270
|
+
'```',
|
|
271
|
+
'',
|
|
272
|
+
'The session uses provider-native Responses items and passthrough context management.',
|
|
273
|
+
'Do not run the Claude warmup importer; it would normalize the native prefix.',
|
|
274
|
+
'',
|
|
275
|
+
].join('\n'));
|
|
276
|
+
|
|
277
|
+
console.log(JSON.stringify({
|
|
278
|
+
instanceDir: opts.outDir,
|
|
279
|
+
dataDir,
|
|
280
|
+
recipePath,
|
|
281
|
+
sessionId: session.id,
|
|
282
|
+
importedItems: history.length,
|
|
283
|
+
compactions: compacted.length,
|
|
284
|
+
snapshotPath,
|
|
285
|
+
}, null, 2));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (import.meta.main) await main();
|