@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.
Files changed (51) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/ci.yml +52 -0
  3. package/.github/workflows/publish.yml +2 -2
  4. package/README.md +5 -0
  5. package/bun.lock +14 -6
  6. package/docs/AGENT-ONBOARDING.md +6 -1
  7. package/package.json +6 -4
  8. package/scripts/import-codex-rollout.ts +288 -0
  9. package/src/call-ledger.ts +371 -0
  10. package/src/call-pricing.ts +119 -0
  11. package/src/framework-agent-config.ts +51 -0
  12. package/src/framework-strategy.ts +70 -0
  13. package/src/index.ts +130 -105
  14. package/src/logging-adapter.ts +105 -12
  15. package/src/mcpl-config.ts +1 -0
  16. package/src/modules/channel-mode-module.ts +14 -16
  17. package/src/modules/fleet-module.ts +17 -7
  18. package/src/modules/mcpl-admin-module.ts +6 -15
  19. package/src/modules/settings-module.ts +83 -59
  20. package/src/modules/subscription-gc-module.ts +17 -20
  21. package/src/modules/time-module.ts +15 -9
  22. package/src/modules/web-ui-module.ts +88 -10
  23. package/src/modules/web-ui-observers.ts +35 -9
  24. package/src/recipe.ts +133 -6
  25. package/src/state/agent-tree-reducer.ts +17 -3
  26. package/src/strategies/frontdesk-strategy.ts +15 -9
  27. package/src/web/protocol.ts +89 -0
  28. package/test/agent-tree-reducer.test.ts +35 -3
  29. package/test/autobio-progress-snapshot.test.ts +27 -12
  30. package/test/call-ledger.test.ts +91 -0
  31. package/test/call-pricing.test.ts +69 -0
  32. package/test/framework-fkm-composition.test.ts +160 -0
  33. package/test/frontdesk-strategy.test.ts +10 -0
  34. package/test/import-codex-rollout.test.ts +36 -0
  35. package/test/logging-adapter-refusal.test.ts +57 -0
  36. package/test/logging-adapter.test.ts +58 -1
  37. package/test/recipe-cache-ttl.test.ts +7 -3
  38. package/test/recipe-compression-fallback.test.ts +36 -0
  39. package/test/recipe-primary-summary-fallback.test.ts +40 -0
  40. package/test/recipe-provider.test.ts +36 -0
  41. package/test/recipe-think-policy.test.ts +39 -0
  42. package/test/recipe-timezone.test.ts +20 -0
  43. package/test/subscription-gc-module.test.ts +7 -5
  44. package/test/time-module.test.ts +13 -0
  45. package/test/web-ui-observers.test.ts +70 -2
  46. package/web/package-lock.json +425 -302
  47. package/web/src/App.tsx +9 -0
  48. package/web/src/ObserverGate.tsx +21 -11
  49. package/web/src/Usage.tsx +116 -2
  50. package/web/src/wire.ts +5 -2
  51. 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.0",
8
+ "@animalabs/agent-framework": "^0.6.7",
9
9
  "@animalabs/chronicle": "^0.2.0",
10
- "@animalabs/context-manager": "^0.5.5",
11
- "@animalabs/membrane": "^0.5.68",
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.0", "", { "dependencies": { "@animalabs/chronicle": "^0.2.0", "@animalabs/context-manager": "^0.5.5", "@animalabs/membrane": "^0.5.64", "chokidar": "^4.0.3", "discord.js": "^14.25.1", "ws": "^8.18.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js" } }, "sha512-LsR2eYveMkC+aZTq0a58bEYoi8+Iq3XQSrSCZ1fbcg+mf9YNOk8ooBRUl0VM03fyPymdOf/fkVx2tdMZM7HjSQ=="],
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.5", "", { "dependencies": { "@animalabs/chronicle": "^0.2.0", "@animalabs/membrane": "^0.5.64" } }, "sha512-KWTluwU5VIZKIPF/IdfUbkvPNZ5zMxSfZjaxZb81IXe+DBPmjHuUVNIqgXIQ2Mv1pCn1vCNqieZ/iSrc6dFH8w=="],
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.68", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-MfSERy0ECxdBZ4NelHdZbqsP/ORWJsfeAI6vuyy6gP77aGcdJL8qf4XrUkOox5yZa5MqB50y0K58Ffv79jCnbg=="],
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=="],
@@ -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' for residents replying slower than 5m (cache re-write premium dominates spend otherwise), omit/'5m' for rapid loops
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.5",
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.0",
16
+ "@animalabs/agent-framework": "^0.6.7",
15
17
  "@animalabs/chronicle": "^0.2.0",
16
- "@animalabs/context-manager": "^0.5.5",
17
- "@animalabs/membrane": "^0.5.68",
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();