@topolo/cli 0.12.33 → 0.13.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.
Files changed (123) hide show
  1. package/README.md +45 -0
  2. package/agent-packs/README.md +495 -0
  3. package/agent-packs/pyproject.toml +16 -0
  4. package/agent-packs/requirements-test.txt +1 -0
  5. package/agent-packs/src/topolo_agent_pack/__init__.py +1 -0
  6. package/agent-packs/src/topolo_agent_pack/blueprint.py +57 -0
  7. package/agent-packs/src/topolo_agent_pack/claude.py +265 -0
  8. package/agent-packs/src/topolo_agent_pack/cli.py +443 -0
  9. package/agent-packs/src/topolo_agent_pack/execution.py +154 -0
  10. package/agent-packs/src/topolo_agent_pack/fresh.py +158 -0
  11. package/agent-packs/src/topolo_agent_pack/live.py +403 -0
  12. package/agent-packs/src/topolo_agent_pack/native.py +645 -0
  13. package/agent-packs/src/topolo_agent_pack/policy.py +92 -0
  14. package/agent-packs/src/topolo_agent_pack/storage.py +271 -0
  15. package/agent-packs/src/topolo_agent_pack/topolo.py +321 -0
  16. package/agent-packs/templates/career/AGENTS.md +17 -0
  17. package/agent-packs/templates/career/README.md +14 -0
  18. package/agent-packs/templates/career/agents/intakes/career-intake/AGENTS.md +21 -0
  19. package/agent-packs/templates/career/agents/specialists/application-preparation/AGENTS.md +21 -0
  20. package/agent-packs/templates/career/agents/specialists/application-tracker/AGENTS.md +21 -0
  21. package/agent-packs/templates/career/agents/specialists/interview-coach/AGENTS.md +21 -0
  22. package/agent-packs/templates/career/agents/specialists/opportunity-research/AGENTS.md +21 -0
  23. package/agent-packs/templates/career/agents/specialists/profile-cv/AGENTS.md +21 -0
  24. package/agent-packs/templates/career/pack.json +124 -0
  25. package/agent-packs/templates/career/shared/PACK.md +22 -0
  26. package/agent-packs/templates/career/shared/TOPOLO.md +13 -0
  27. package/agent-packs/templates/career/shared/WORKFLOW.md +13 -0
  28. package/agent-packs/templates/career/skills/topolo/SKILL.md +305 -0
  29. package/agent-packs/templates/clients/AGENTS.md +17 -0
  30. package/agent-packs/templates/clients/README.md +14 -0
  31. package/agent-packs/templates/clients/agents/intakes/client-intake/AGENTS.md +21 -0
  32. package/agent-packs/templates/clients/agents/specialists/acceptance-review/AGENTS.md +21 -0
  33. package/agent-packs/templates/clients/agents/specialists/delivery-planner/AGENTS.md +21 -0
  34. package/agent-packs/templates/clients/agents/specialists/discovery/AGENTS.md +21 -0
  35. package/agent-packs/templates/clients/agents/specialists/proposal-writer/AGENTS.md +21 -0
  36. package/agent-packs/templates/clients/agents/specialists/solution-design/AGENTS.md +21 -0
  37. package/agent-packs/templates/clients/pack.json +126 -0
  38. package/agent-packs/templates/clients/shared/PACK.md +22 -0
  39. package/agent-packs/templates/clients/shared/TOPOLO.md +13 -0
  40. package/agent-packs/templates/clients/shared/WORKFLOW.md +13 -0
  41. package/agent-packs/templates/clients/skills/topolo/SKILL.md +305 -0
  42. package/agent-packs/templates/creative/AGENTS.md +17 -0
  43. package/agent-packs/templates/creative/README.md +14 -0
  44. package/agent-packs/templates/creative/agents/intakes/creative-director/AGENTS.md +21 -0
  45. package/agent-packs/templates/creative/agents/specialists/brand-copy/AGENTS.md +21 -0
  46. package/agent-packs/templates/creative/agents/specialists/media-production/AGENTS.md +21 -0
  47. package/agent-packs/templates/creative/agents/specialists/quality-review/AGENTS.md +21 -0
  48. package/agent-packs/templates/creative/agents/specialists/storytelling/AGENTS.md +21 -0
  49. package/agent-packs/templates/creative/agents/specialists/visual-design/AGENTS.md +21 -0
  50. package/agent-packs/templates/creative/pack.json +128 -0
  51. package/agent-packs/templates/creative/shared/PACK.md +22 -0
  52. package/agent-packs/templates/creative/shared/TOPOLO.md +13 -0
  53. package/agent-packs/templates/creative/shared/WORKFLOW.md +13 -0
  54. package/agent-packs/templates/creative/skills/topolo/SKILL.md +305 -0
  55. package/agent-packs/templates/founder/AGENTS.md +17 -0
  56. package/agent-packs/templates/founder/README.md +14 -0
  57. package/agent-packs/templates/founder/agents/intakes/founder-intake/AGENTS.md +21 -0
  58. package/agent-packs/templates/founder/agents/specialists/commercial-modelling/AGENTS.md +21 -0
  59. package/agent-packs/templates/founder/agents/specialists/customer-research/AGENTS.md +21 -0
  60. package/agent-packs/templates/founder/agents/specialists/decision-review/AGENTS.md +21 -0
  61. package/agent-packs/templates/founder/agents/specialists/partnerships/AGENTS.md +21 -0
  62. package/agent-packs/templates/founder/agents/specialists/strategy/AGENTS.md +21 -0
  63. package/agent-packs/templates/founder/pack.json +126 -0
  64. package/agent-packs/templates/founder/shared/PACK.md +22 -0
  65. package/agent-packs/templates/founder/shared/TOPOLO.md +13 -0
  66. package/agent-packs/templates/founder/shared/WORKFLOW.md +13 -0
  67. package/agent-packs/templates/founder/skills/topolo/SKILL.md +305 -0
  68. package/agent-packs/templates/learning/AGENTS.md +17 -0
  69. package/agent-packs/templates/learning/README.md +14 -0
  70. package/agent-packs/templates/learning/agents/intakes/learning-intake/AGENTS.md +21 -0
  71. package/agent-packs/templates/learning/agents/specialists/critical-reviewer/AGENTS.md +21 -0
  72. package/agent-packs/templates/learning/agents/specialists/curriculum-designer/AGENTS.md +21 -0
  73. package/agent-packs/templates/learning/agents/specialists/knowledge-steward/AGENTS.md +21 -0
  74. package/agent-packs/templates/learning/agents/specialists/practice-coach/AGENTS.md +21 -0
  75. package/agent-packs/templates/learning/agents/specialists/tutor/AGENTS.md +21 -0
  76. package/agent-packs/templates/learning/pack.json +124 -0
  77. package/agent-packs/templates/learning/shared/PACK.md +22 -0
  78. package/agent-packs/templates/learning/shared/TOPOLO.md +13 -0
  79. package/agent-packs/templates/learning/shared/WORKFLOW.md +13 -0
  80. package/agent-packs/templates/learning/skills/topolo/SKILL.md +305 -0
  81. package/agent-packs/templates/personal/AGENTS.md +17 -0
  82. package/agent-packs/templates/personal/README.md +14 -0
  83. package/agent-packs/templates/personal/agents/intakes/personal-intake/AGENTS.md +21 -0
  84. package/agent-packs/templates/personal/agents/specialists/correspondence/AGENTS.md +21 -0
  85. package/agent-packs/templates/personal/agents/specialists/document-assistant/AGENTS.md +21 -0
  86. package/agent-packs/templates/personal/agents/specialists/planning/AGENTS.md +21 -0
  87. package/agent-packs/templates/personal/agents/specialists/research/AGENTS.md +21 -0
  88. package/agent-packs/templates/personal/agents/specialists/travel-logistics/AGENTS.md +21 -0
  89. package/agent-packs/templates/personal/pack.json +125 -0
  90. package/agent-packs/templates/personal/shared/PACK.md +22 -0
  91. package/agent-packs/templates/personal/shared/TOPOLO.md +13 -0
  92. package/agent-packs/templates/personal/shared/WORKFLOW.md +13 -0
  93. package/agent-packs/templates/personal/skills/topolo/SKILL.md +305 -0
  94. package/agent-packs/templates/research/AGENTS.md +17 -0
  95. package/agent-packs/templates/research/README.md +14 -0
  96. package/agent-packs/templates/research/agents/intakes/research-intake/AGENTS.md +21 -0
  97. package/agent-packs/templates/research/agents/specialists/adversarial-review/AGENTS.md +21 -0
  98. package/agent-packs/templates/research/agents/specialists/data-analysis/AGENTS.md +21 -0
  99. package/agent-packs/templates/research/agents/specialists/source-research/AGENTS.md +21 -0
  100. package/agent-packs/templates/research/agents/specialists/synthesis/AGENTS.md +21 -0
  101. package/agent-packs/templates/research/agents/specialists/technical-research/AGENTS.md +21 -0
  102. package/agent-packs/templates/research/pack.json +124 -0
  103. package/agent-packs/templates/research/shared/PACK.md +22 -0
  104. package/agent-packs/templates/research/shared/TOPOLO.md +13 -0
  105. package/agent-packs/templates/research/shared/WORKFLOW.md +13 -0
  106. package/agent-packs/templates/research/skills/topolo/SKILL.md +305 -0
  107. package/agent-packs/templates/ventures/AGENTS.md +17 -0
  108. package/agent-packs/templates/ventures/README.md +14 -0
  109. package/agent-packs/templates/ventures/agents/intakes/venture-intake/AGENTS.md +21 -0
  110. package/agent-packs/templates/ventures/agents/specialists/customer-discovery/AGENTS.md +21 -0
  111. package/agent-packs/templates/ventures/agents/specialists/economics/AGENTS.md +21 -0
  112. package/agent-packs/templates/ventures/agents/specialists/experiment-design/AGENTS.md +21 -0
  113. package/agent-packs/templates/ventures/agents/specialists/market-research/AGENTS.md +21 -0
  114. package/agent-packs/templates/ventures/agents/specialists/product-concept/AGENTS.md +21 -0
  115. package/agent-packs/templates/ventures/pack.json +126 -0
  116. package/agent-packs/templates/ventures/shared/PACK.md +22 -0
  117. package/agent-packs/templates/ventures/shared/TOPOLO.md +13 -0
  118. package/agent-packs/templates/ventures/shared/WORKFLOW.md +13 -0
  119. package/agent-packs/templates/ventures/skills/topolo/SKILL.md +305 -0
  120. package/agent-packs/topolo-agent-pack +8 -0
  121. package/dist/claude-pack-backend-IDQQX3DG.js +555 -0
  122. package/dist/index.js +143 -58
  123. package/package.json +7 -4
@@ -0,0 +1,645 @@
1
+ """Versioned local Codex storage adapter. Never writes to capture sources."""
2
+ import base64
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import pathlib
8
+ import re
9
+ import shutil
10
+ import sqlite3
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ import tomllib
15
+
16
+ from .storage import PackError, atomic_json, canonical, component_key, now, select, slug
17
+
18
+ DB_TABLES = {
19
+ 'state_5.sqlite': {'_sqlx_migrations', 'threads', 'thread_sections', 'projects', 'project_roots', 'thread_attachments', 'thread_dynamic_tools', 'thread_spawn_edges', 'backfill_state', 'remote_control_enrollments', 'external_agent_config_imports', 'rollout_migration_state', 'rollout_migration_skipped_rollouts', 'project_idempotency_keys'},
20
+ 'thread_history_1.sqlite': {'_sqlx_migrations', 'thread_turns', 'thread_items', 'thread_history_projection_state', 'thread_realtime_items'},
21
+ 'goals_1.sqlite': {'_sqlx_migrations', 'thread_goals', 'thread_goal_continuation_deferrals'},
22
+ }
23
+ SKIP_DIRS = {'.git', 'node_modules', '.venv', 'venv', '__pycache__', '.cache', '.next', '.DS_Store'}
24
+ SECRET_NAMES = {'auth.json', '.npmrc', '.pypirc', 'credentials.json', 'credentials', 'id_rsa', 'id_ed25519', '.netrc'}
25
+ NEVER_COPY_ROWS = {'backfill_state','remote_control_enrollments','external_agent_config_imports','rollout_migration_state','rollout_migration_skipped_rollouts','project_idempotency_keys'}
26
+
27
+ def quote(name):
28
+ return '"' + name.replace('"', '""') + '"'
29
+
30
+ def ro(path):
31
+ return sqlite3.connect('file:' + str(pathlib.Path(path).resolve()) + '?mode=ro', uri=True)
32
+
33
+ def encode(value):
34
+ if isinstance(value, bytes): return {'$blob': base64.b64encode(value).decode()}
35
+ return value
36
+
37
+ def decode(value):
38
+ if isinstance(value, dict) and set(value) == {'$blob'}: return base64.b64decode(value['$blob'], validate=True)
39
+ return value
40
+
41
+ def busy_writers(home):
42
+ home = pathlib.Path(home).resolve()
43
+ files = [str(p) for name in DB_TABLES for suffix in ['', '-wal'] if (p := home / (name + suffix)).exists()]
44
+ pids = set()
45
+ if sys.platform == 'darwin' and files:
46
+ result = subprocess.run(['/usr/sbin/lsof', '-F', 'p', '--', *files], capture_output=True, text=True, timeout=15)
47
+ if result.returncode not in (0, 1): raise PackError('Cannot establish that the source databases are idle (lsof failed).')
48
+ pids = {int(line[1:]) for line in result.stdout.splitlines() if line.startswith('p') and line[1:].isdigit()}
49
+ elif pathlib.Path('/proc').exists():
50
+ targets = set(files)
51
+ denied = False
52
+ for proc in pathlib.Path('/proc').iterdir():
53
+ if not proc.name.isdigit() or int(proc.name) == os.getpid(): continue
54
+ try:
55
+ for fd in (proc / 'fd').iterdir():
56
+ try:
57
+ if os.readlink(fd) in targets: pids.add(int(proc.name))
58
+ except FileNotFoundError: pass
59
+ except (PermissionError, FileNotFoundError):
60
+ try:
61
+ if proc.stat().st_uid == os.getuid(): denied = True
62
+ except FileNotFoundError: pass
63
+ if denied: raise PackError('Cannot inspect all same-user database holders; close Codex before retrying.')
64
+ else:
65
+ raise PackError('This platform has no verified idle-runtime detector. Capture/activation requires macOS or Linux.')
66
+ pids.discard(os.getpid())
67
+ return sorted(pids)
68
+
69
+ def assert_idle(home):
70
+ pids = busy_writers(home)
71
+ if pids:
72
+ raise PackError(f'Codex databases are open in processes {pids}. This holder is not a quiesced managed backend. Use a topolo-agent-pack managed window for live switching. No process was stopped.')
73
+
74
+ def signature(home):
75
+ home = pathlib.Path(home)
76
+ names = [name + suffix for name in DB_TABLES for suffix in ['', '-wal']] + ['.codex-global-state.json']
77
+ return {name: (p.stat().st_size, p.stat().st_mtime_ns) for name in names if (p := home / name).exists() and (not name.endswith('-wal') or p.stat().st_size>0)}
78
+
79
+ def display_name(row, global_state):
80
+ names = global_state.get('thread-titles', {})
81
+ if isinstance(names.get('titles'), dict): names = names['titles']
82
+ return row.get('name') or names.get(row['id']) or row.get('title') or row['id']
83
+
84
+ def schemas_and_rows(connection, tables, thread_ids):
85
+ ids = sorted(thread_ids)
86
+ marks = ','.join('?' for _ in ids)
87
+ schemas = []
88
+ rows = {}
89
+ for kind, name, table, sql in connection.execute("SELECT type,name,tbl_name,sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY CASE type WHEN 'table' THEN 0 ELSE 1 END"):
90
+ if table not in tables or kind not in {'table', 'index'}: continue
91
+ schemas.append({'type': kind, 'name': name, 'table': table, 'sql': sql})
92
+ if kind != 'table': continue
93
+ cols = [x[1] for x in connection.execute('PRAGMA table_info(' + quote(name) + ')')]
94
+ if name in NEVER_COPY_ROWS: query, args = 'SELECT * FROM ' + quote(name) + ' WHERE 0', []
95
+ elif name == '_sqlx_migrations': query, args = 'SELECT * FROM ' + quote(name), []
96
+ elif name == 'threads': query, args = f'SELECT * FROM threads WHERE id IN ({marks})', ids
97
+ elif name == 'thread_sections': query, args = f'SELECT * FROM thread_sections WHERE id IN (SELECT thread_section_id FROM threads WHERE id IN ({marks}))', ids
98
+ elif name == 'projects': query, args = f'SELECT * FROM projects WHERE id IN (SELECT project_id FROM threads WHERE id IN ({marks}))', ids
99
+ elif name == 'project_roots': query, args = f'SELECT * FROM project_roots WHERE project_id IN (SELECT project_id FROM threads WHERE id IN ({marks}))', ids
100
+ elif name == 'thread_spawn_edges': query, args = f'SELECT * FROM thread_spawn_edges WHERE parent_thread_id IN ({marks}) AND child_thread_id IN ({marks})', ids + ids
101
+ elif 'thread_id' in cols: query, args = f'SELECT * FROM {quote(name)} WHERE thread_id IN ({marks})', ids
102
+ else: query, args = 'SELECT * FROM ' + quote(name) + ' WHERE 0', []
103
+ rows[name] = {'columns': cols, 'rows': [[encode(v) for v in row] for row in connection.execute(query, args)]}
104
+ return {'schemas': schemas, 'tables': rows}
105
+
106
+ def unregistered_threads(home,known):
107
+ db=ro(pathlib.Path(home)/'state_5.sqlite')
108
+ try:
109
+ children={r[0] for r in db.execute('SELECT child_thread_id FROM thread_spawn_edges')}
110
+ return [r[0] for r in db.execute('SELECT id FROM threads WHERE archived=0') if r[0] not in known and r[0] not in children]
111
+ finally:db.close()
112
+
113
+ def capture(home, library, name, sets=None, agents=None, include=None, all_threads=False, template=None, workspace_mode='copy', live=False):
114
+ home = pathlib.Path(home).expanduser().resolve()
115
+ if not (home / 'state_5.sqlite').exists(): raise PackError(f'No Codex thread catalog in {home}')
116
+ if home == library.root or home in library.root.parents: raise PackError('Pack library must be outside the captured Codex home.')
117
+ if not live: assert_idle(home)
118
+ initial = signature(home)
119
+ gs = json.loads((home / '.codex-global-state.json').read_text()) if (home / '.codex-global-state.json').exists() else {}
120
+ connections={}
121
+ try:
122
+ for filename in DB_TABLES:
123
+ if (home/filename).exists():
124
+ connection=ro(home/filename);connections[filename]=connection
125
+ if live:
126
+ connection.execute('BEGIN')
127
+ connection.execute('SELECT name FROM sqlite_master LIMIT 1').fetchone()
128
+ db=connections['state_5.sqlite'];db.row_factory=sqlite3.Row
129
+ available = {row['id']: dict(row) for row in db.execute('SELECT * FROM threads')}
130
+ section_names = {row['id']: row['name'] for row in db.execute('SELECT * FROM thread_sections')}
131
+ visible = []
132
+ if template:
133
+ visible = copy.deepcopy(template['agents'])
134
+ for a in visible:
135
+ if a['threadId'] not in available: raise PackError('Active runtime lost an agent record: ' + a['threadId'])
136
+ a['title'] = display_name(available[a['threadId']], gs)
137
+ if all_threads:
138
+ known={a['threadId'] for a in visible}
139
+ children={r[0] for r in db.execute('SELECT child_thread_id FROM thread_spawn_edges')}
140
+ keys={component_key(a) for a in visible}
141
+ for tid,row in available.items():
142
+ if tid in known or tid in children or row.get('archived'):continue
143
+ section=section_names.get(row.get('thread_section_id'),'Recents');title=display_name(row,gs)
144
+ set_id='intakes' if section.lower()=='pinned' else slug(re.sub('^'+re.escape(name)+r'\s+','',section,flags=re.I))
145
+ agent_id=slug(title)
146
+ if name+'/'+set_id+'/'+agent_id in keys:agent_id=agent_id[:48]+'-'+tid[-8:]
147
+ a={'pack':name,'set':set_id,'id':agent_id,'aliases':[],'threadId':tid,'title':title,'sectionName':section,'pinned':section.lower()=='pinned','position':row.get('section_position') or 0}
148
+ visible.append(a);keys.add(component_key(a))
149
+ else:
150
+ matching = {sid for sid, label in section_names.items() if label.lower().startswith(name.lower() + ' ')}
151
+ for tid, row in available.items():
152
+ if row.get('archived') or row.get('source', '').startswith('subagent'): continue
153
+ section = section_names.get(row.get('thread_section_id'), 'Tasks')
154
+ title = display_name(row, gs)
155
+ pinned = section.lower() == 'pinned'
156
+ if not all_threads and matching:
157
+ if row.get('thread_section_id') not in matching and not (pinned and title.lower().startswith(name.lower())): continue
158
+ elif not all_threads:
159
+ raise PackError(f'No sections belong to {name!r}. Supply --all for an explicit whole-home capture.')
160
+ set_id = 'intakes' if pinned else slug(re.sub('^' + re.escape(name) + r'\s+', '', section, flags=re.I))
161
+ agent_id = slug(re.sub('^' + re.escape(name) + r'\s+', '', title, flags=re.I))
162
+ aliases = [agent_id.split('-')[0]]
163
+ visible.append({'pack': name, 'set': set_id, 'id': agent_id, 'aliases': aliases, 'threadId': tid, 'title': title,
164
+ 'sectionName': section, 'pinned': pinned, 'position': row.get('section_position') or 0})
165
+ chosen = select({'agents': visible}, sets, agents)
166
+ if len({component_key(a) for a in chosen}) != len(chosen): raise PackError('Duplicate semantic identities in source; rename or capture scoped sets.')
167
+ closure = {a['threadId'] for a in chosen}; pending = list(closure); threads = {}
168
+ while pending:
169
+ tid = pending.pop()
170
+ if tid not in available: raise PackError('Required inherited/child history is missing: ' + tid)
171
+ row = available[tid]; path = pathlib.Path(row['rollout_path'])
172
+ if not path.is_file(): raise PackError('Required rollout is missing: ' + str(path))
173
+ with path.open('rb') as f: header = json.loads(f.readline()).get('payload', {})
174
+ parent = (header.get('history_base') or {}).get('thread_id')
175
+ children = [r[0] for r in db.execute('SELECT child_thread_id FROM thread_spawn_edges WHERE parent_thread_id=?', (tid,))]
176
+ threads[tid] = {'basename': path.name, 'cwd': row['cwd'], 'historyBase': parent, 'children': children}
177
+ for related in ([parent] if parent else []) + children:
178
+ if related not in closure: closure.add(related); pending.append(related)
179
+ cuts={}
180
+ if live:
181
+ history=connections.get('thread_history_1.sqlite')
182
+ for tid in closure:
183
+ path=pathlib.Path(available[tid]['rollout_path'])
184
+ if available[tid].get('history_mode')=='paginated':
185
+ row=history.execute('SELECT next_rollout_byte_offset FROM thread_history_projection_state WHERE thread_id=?',(tid,)).fetchone() if history else None
186
+ if not row: raise PackError('History projection is not ready for '+tid+'; retry after its current turn settles. Codex can stay open.')
187
+ cuts[tid]=row[0]
188
+ else:
189
+ with path.open('rb') as stream:
190
+ end=path.stat().st_size
191
+ while end>0:
192
+ begin=max(0,end-65536);stream.seek(begin);chunk=stream.read(end-begin)
193
+ index=chunk.rfind(b'\n')
194
+ if index>=0: end=begin+index+1;break
195
+ end=begin
196
+ cuts[tid]=end
197
+ for tid in closure:
198
+ path=pathlib.Path(available[tid]['rollout_path'])
199
+ with path.open('rb') as stream: base=json.loads(stream.readline()).get('payload',{}).get('history_base')
200
+ if base and base.get('end_byte_offset') is not None: cuts[base['thread_id']]=max(cuts.get(base['thread_id'],0),base['end_byte_offset'])
201
+ for tid,cut in cuts.items():
202
+ path=pathlib.Path(available[tid]['rollout_path'])
203
+ with path.open('rb') as stream:
204
+ header_end=len(stream.readline())
205
+ if cut<header_end: raise PackError('History projection has not saved its header; retry capture')
206
+ stream.seek(cut-1)
207
+ if stream.read(1)!=b'\n': raise PackError('History snapshot has an incomplete rollout boundary; retry capture')
208
+ for tid in closure:
209
+ threads[tid]['rollout']=library.put(pathlib.Path(available[tid]['rollout_path']),limit=cuts.get(tid) if live else None)
210
+ catalog = {}
211
+ for filename, allowed in DB_TABLES.items():
212
+ if (home / filename).exists():
213
+ catalog[filename] = schemas_and_rows(connections[filename], allowed, closure)
214
+ if any(available[tid].get('history_mode') == 'paginated' for tid in closure) and 'thread_history_1.sqlite' not in catalog:
215
+ raise PackError('Paginated history database is missing; capture would be incomplete.')
216
+ resources = []; warnings = []; roots = {}
217
+ if live: warnings.append('Live capture uses read-only database snapshots and complete rollout prefixes. Work started after the snapshot remains in the source profile.')
218
+ if workspace_mode == 'copy':
219
+ for tid in closure:
220
+ cwd = pathlib.Path(available[tid]['cwd']).absolute()
221
+ if not cwd.is_dir(): raise PackError(f'Working directory missing: {cwd}; use --workspace-mode reference and supply a destination --map.')
222
+ if cwd.resolve() == pathlib.Path.home() or cwd == pathlib.Path('/') or cwd.resolve() == home or cwd.resolve() in home.parents:
223
+ raise PackError(f'Refusing an unbounded/credential-containing workspace capture: {cwd}')
224
+ roots[str(cwd)] = 'workspaces/' + hashlib.sha256(str(cwd).encode()).hexdigest()[:16]
225
+ else:
226
+ warnings.append('Workspaces are external references; destination paths must exist or be supplied using --map.')
227
+ for location in list(include or [])+list((template or {}).get('resourcePaths',[])):
228
+ path = pathlib.Path(location).expanduser().resolve()
229
+ if not path.exists(): raise PackError('Included resource is missing: ' + str(path))
230
+ if path == pathlib.Path('/') or path == pathlib.Path.home() or path == home or path in home.parents:
231
+ raise PackError('Refusing unbounded resource inclusion')
232
+ roots.setdefault(str(path),'resources/' + hashlib.sha256(str(path).encode()).hexdigest()[:16] + '/' + path.name)
233
+ seen = set()
234
+ for origin in roots:
235
+ root = pathlib.Path(origin)
236
+ paths = []
237
+ if root.is_file(): paths = [root]
238
+ else:
239
+ for folder, dirs, files in os.walk(root, followlinks=False):
240
+ folder = pathlib.Path(folder)
241
+ omitted = [x for x in dirs if x in SKIP_DIRS or (folder / x).resolve() == library.root or (folder / x).is_symlink()]
242
+ dirs[:] = [x for x in dirs if x not in omitted]
243
+ if omitted: warnings.append('Excluded generated/symlink directories under ' + str(folder) + ': ' + ','.join(omitted))
244
+ paths.extend(folder / x for x in files)
245
+ for path in paths:
246
+ if path.is_symlink():
247
+ warnings.append('External symlink not packaged: ' + str(path)); continue
248
+ if path.name in SECRET_NAMES or path.name.startswith('.env') or path.suffix in {'.pem', '.key', '.p12', '.pfx'}:
249
+ warnings.append('Credential/environment file excluded: ' + str(path)); continue
250
+ if not path.is_file() or str(path) in seen: continue
251
+ seen.add(str(path)); resources.append({'original': str(path), 'content': library.put(path), 'executable': bool(path.stat().st_mode & 0o111)})
252
+ # Local instruction/skill definitions are portable; credentials and plugin caches are not.
253
+ definitions = []
254
+ for folder in ['skills', 'agents']:
255
+ source = home / folder
256
+ if source.exists():
257
+ for path in source.rglob('*'):
258
+ if path.is_file() and not path.is_symlink() and not any(p in SKIP_DIRS for p in path.parts) and path.name not in SECRET_NAMES and not path.name.startswith('.env') and path.suffix not in {'.key','.pem','.p12','.pfx'}:
259
+ definitions.append({'relative': str(path.relative_to(home)), 'content': library.put(path)})
260
+ if (home/'AGENTS.md').is_file(): definitions.append({'relative':'AGENTS.md','content':library.put(home/'AGENTS.md')})
261
+ config = {}
262
+ dependencies = copy.deepcopy((template or {}).get('dependencies',{}))
263
+ config_path = home / 'config.toml'
264
+ if config_path.exists():
265
+ raw = tomllib.loads(config_path.read_text())
266
+ for key in ['model', 'model_reasoning_effort', 'personality']:
267
+ if isinstance(raw.get(key), str): config[key] = raw[key]
268
+ dependencies.setdefault('mcpServers',{}).update({k: {'transport': 'url' if 'url' in v else 'command', 'requiredEnvironment': sorted(v.get('env', {}))} for k,v in raw.get('mcp_servers', {}).items()})
269
+ dependencies['plugins'] = sorted(set(dependencies.get('plugins',[])) | set(raw.get('plugins', {})))
270
+ if dependencies['mcpServers'] or dependencies['plugins']:
271
+ warnings.append('Account-scoped MCP/plugin dependencies are declared, not copied with credentials; configure them locally before agent work.')
272
+ aliases=(template or {}).get('rootOrigins',{})
273
+ if aliases:
274
+ # Keep stable resource identities across checkpoints of relocated runtimes.
275
+ roots={remap(k,aliases):v for k,v in roots.items()}
276
+ for resource in resources: resource['original']=remap(resource['original'],aliases)
277
+ for thread in threads.values(): thread['cwd']=remap(thread['cwd'],aliases)
278
+ descriptions = gs.get('electron-persisted-atom-state', {}).get('thread-descriptions-v1', {})
279
+ manifest = {'format': 'topolo-agent-pack', 'schemaVersion': 1, 'packId': name, 'capturedAt': now(), 'agents': chosen,
280
+ 'threads': threads, 'catalog': library.put(canonical(catalog)), 'resourceRoots': roots, 'resources': resources,
281
+ 'definitions': definitions, 'resourceAliases':aliases, 'runtimeCheckpoint':bool(template), 'config': config, 'dependencies': dependencies, 'warnings': sorted(set(warnings)),
282
+ 'workspaceMode': workspace_mode, 'captureMode':'live-snapshot' if live else 'idle', 'ui': {'descriptions': {k:v for k,v in descriptions.items() if any(tid in k for tid in closure)}, 'sourceAccountIds': list(gs.get('electron-persisted-atom-state', {}).get('sidebar-custom-sections-v3', {}))}}
283
+ finally:
284
+ for connection in connections.values():connection.close()
285
+ if not live:
286
+ assert_idle(home)
287
+ if initial != signature(home): raise PackError('Codex storage changed during capture. No complete snapshot was published; retry after closing the source profile.')
288
+ revision = library.save(manifest)
289
+ return manifest, revision
290
+
291
+ def remap(value, mappings):
292
+ if not isinstance(value, str): return value
293
+ if value.startswith('file://'):
294
+ from urllib.parse import urlsplit,unquote,quote as urlquote
295
+ uri=urlsplit(value)
296
+ if uri.netloc not in {'','localhost'}: return value
297
+ original=unquote(uri.path);changed=remap(original,mappings)
298
+ return 'file://'+urlquote(changed,safe='/') if changed!=original else value
299
+ for old, new in sorted(mappings.items(), key=lambda x: -len(x[0])):
300
+ if value == old or value.startswith(old + os.sep): return new + value[len(old):]
301
+ return value
302
+
303
+ def path_fields(value, mappings, parent_key=''):
304
+ fields = {'cwd','path','rollout_path','workspace_roots','runtime_workspace_roots','writable_roots','working_directory','roots'}
305
+ if isinstance(value, dict): return {k:path_fields(v,mappings,k) for k,v in value.items()}
306
+ if isinstance(value, list): return [path_fields(x,mappings,parent_key) for x in value]
307
+ if parent_key in fields: return remap(value,mappings)
308
+ return value
309
+
310
+ def runtime_record(obj):
311
+ return obj.get('type') in {'session_meta','turn_context','world_state'} or (obj.get('type')=='event_msg' and obj.get('payload',{}).get('type')=='thread_settings_applied')
312
+
313
+ def read_catalog(library, manifest):
314
+ return json.loads(library.object(manifest['catalog']).read_text())
315
+
316
+ def slice_manifest(library,manifest,chosen):
317
+ result=copy.deepcopy(manifest);closure=set();pending=[a['threadId'] for a in chosen]
318
+ while pending:
319
+ tid=pending.pop()
320
+ if tid in closure: continue
321
+ closure.add(tid);t=manifest['threads'][tid]
322
+ pending.extend(([t['historyBase']] if t.get('historyBase') else [])+t.get('children',[]))
323
+ result['agents']=chosen;result['threads']={k:v for k,v in manifest['threads'].items() if k in closure}
324
+ catalog=read_catalog(library,manifest)
325
+ records=catalog['state_5.sqlite']['tables']['threads']
326
+ rows=[dict(zip(records['columns'],r)) for r in records['rows'] if r[records['columns'].index('id')] in closure]
327
+ sections={r.get('thread_section_id') for r in rows};projects={r.get('project_id') for r in rows}
328
+ for data in catalog.values():
329
+ for table,entry in data['tables'].items():
330
+ cols=entry['columns']
331
+ def keep(values):
332
+ row=dict(zip(cols,values))
333
+ if table=='threads': return row['id'] in closure
334
+ if 'thread_id' in cols: return row['thread_id'] in closure
335
+ if table=='thread_sections': return row['id'] in sections
336
+ if table=='projects': return row['id'] in projects
337
+ if table=='project_roots': return row['project_id'] in projects
338
+ if table=='thread_spawn_edges': return row['parent_thread_id'] in closure and row['child_thread_id'] in closure
339
+ return True
340
+ entry['rows']=[r for r in entry['rows'] if keep(r)]
341
+ result['catalog']=library.put(canonical(catalog))
342
+ cwds=[pathlib.Path(t['cwd']) for t in result['threads'].values()]
343
+ result['resourceRoots']={k:v for k,v in result['resourceRoots'].items() if not v.startswith('workspaces/') or any(c.is_relative_to(k) for c in cwds)}
344
+ result['resources']=[r for r in result['resources'] if any(pathlib.Path(r['original']).is_relative_to(k) for k in result['resourceRoots'])]
345
+ result['ui']['descriptions']={k:v for k,v in result['ui']['descriptions'].items() if any(t in k for t in closure)}
346
+ result['selectionInstalledAt']=now()
347
+ return result
348
+
349
+ def same_history(library, left, right):
350
+ """Relocated typed paths and inherited byte offsets do not change saved history."""
351
+ def normalize(value, key=''):
352
+ if isinstance(value,dict): return {k:normalize(v,k) for k,v in value.items() if k!='end_byte_offset'}
353
+ if isinstance(value,list): return [normalize(v,key) for v in value]
354
+ if key in {'cwd','path','rollout_path','workspace_roots','runtime_workspace_roots','writable_roots','working_directory','roots'}: return '<relocated-path>'
355
+ return value
356
+ from itertools import zip_longest
357
+ with library.object(left).open('rb') as a, library.object(right).open('rb') as b:
358
+ for x,y in zip_longest(a,b):
359
+ if x is None or y is None: return False
360
+ if x==y: continue
361
+ x=json.loads(x);y=json.loads(y)
362
+ if runtime_record(x): x['payload']=normalize(x.get('payload',{}))
363
+ if runtime_record(y): y['payload']=normalize(y.get('payload',{}))
364
+ if x!=y: return False
365
+ return True
366
+
367
+ def materialize(library, components, runtime, mappings=None):
368
+ """Build a fresh, manager-owned runtime; byte offsets are recomputed after path rebasing."""
369
+ runtime = pathlib.Path(runtime).resolve(); home = runtime / 'home'
370
+ if runtime.exists(): raise PackError('Target runtime already exists; never overwrite it.')
371
+ loaded = {}
372
+ chosen = []; thread_sources = {}; manifests = []; visible = set()
373
+ for component in components:
374
+ source = (component['sourcePack'], component['revision'])
375
+ if source not in loaded: loaded[source] = library.load(*source)[0]
376
+ manifest = loaded[source]
377
+ agent = next((a for a in manifest['agents'] if component_key(a) == component['key']), None)
378
+ if agent is None: raise PackError('Composition references missing agent: ' + component['key'])
379
+ if agent['threadId'] in visible: raise PackError('Two active agents refer to the same native thread identity; create an independently namespaced pack first.')
380
+ visible.add(agent['threadId']); chosen.append(agent)
381
+ if manifest not in manifests: manifests.append(manifest)
382
+ pending = [agent['threadId']]
383
+ while pending:
384
+ tid = pending.pop()
385
+ thread = manifest['threads'][tid]
386
+ if tid in thread_sources:
387
+ prior = thread_sources[tid][1]
388
+ if prior['rollout'] != thread['rollout'] and not same_history(library,prior['rollout'],thread['rollout']): raise PackError('Conflicting revisions of shared history: ' + tid)
389
+ continue
390
+ thread_sources[tid] = (manifest,thread)
391
+ pending.extend(([thread['historyBase']] if thread.get('historyBase') else []) + thread.get('children', []))
392
+ required=sum(t['rollout']['bytes'] for _,t in thread_sources.values())
393
+ required+=sum(m['catalog']['bytes']*2+sum(r['content']['bytes'] for r in m['resources'])+sum(d['content']['bytes'] for d in m['definitions']) for m in manifests)
394
+ required+=128*1024*1024
395
+ if shutil.disk_usage(library.root).free<required:raise PackError('Insufficient local space to stage this composition while preserving its previous runtime; estimated requirement '+str(required)+' bytes')
396
+ runtime.mkdir(parents=True,mode=0o700);home.mkdir(mode=0o700)
397
+ mapping = dict(mappings or {})
398
+ resource_roots = {}
399
+ # Establish all shared root bindings before writing any pack's files.
400
+ for manifest in manifests:
401
+ for old, relative in manifest['resourceRoots'].items():
402
+ if pathlib.Path(relative).is_absolute() or '..' in pathlib.Path(relative).parts: raise PackError('Unsafe resource root')
403
+ if old not in mapping: mapping[old] = str(runtime / 'files' / relative)
404
+ resource_roots[old] = mapping[old]
405
+ for manifest in manifests:
406
+ for old,original in manifest.get('resourceAliases',{}).items(): mapping[old]=remap(original,mapping)
407
+ if manifest.get('startingContext',{}).get('kind')=='fresh':
408
+ seed_home=manifest['startingContext'].get('seedHome')
409
+ if seed_home:
410
+ mapping[seed_home]=str(home)
411
+ mapping[str(pathlib.Path(seed_home).resolve())]=str(home)
412
+ written_resources={};written_definitions={}
413
+ def checkpoint_owns(owner,donor):
414
+ return owner.get('runtimeCheckpoint') and bool({a['pack'] for a in owner['agents']} & {a['pack'] for a in donor['agents']})
415
+ def content(reference,rebase=False):
416
+ data=library.object(reference).read_bytes()
417
+ if rebase:
418
+ text=data.decode()
419
+ for old,new in sorted(mapping.items(),key=lambda x:-len(x[0])):text=text.replace(old,new)
420
+ data=text.encode()
421
+ return data
422
+ for manifest in manifests:
423
+ for resource in manifest['resources']:
424
+ target = pathlib.Path(remap(resource['original'],mapping))
425
+ if target == pathlib.Path(resource['original']) or not target.is_relative_to(runtime):
426
+ # Explicit external mappings are references, never write into them.
427
+ if target.exists(): continue
428
+ raise PackError('Resource target needs a manager-owned mapping: ' + str(target))
429
+ target.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
430
+ data=content(resource['content'],target.name=='AGENTS.md')
431
+ if target.exists() and target.read_bytes()!=data:
432
+ if target in written_resources and checkpoint_owns(written_resources[target],manifest): continue
433
+ raise PackError('Conflicting shared resource: ' + str(target))
434
+ target.write_bytes(data); target.chmod(0o700 if resource['executable'] else 0o600)
435
+ written_resources[target]=manifest
436
+ for definition in manifest['definitions']:
437
+ relative = pathlib.Path(definition['relative'])
438
+ if relative.is_absolute() or '..' in relative.parts or (relative.parts[0] not in {'skills','agents'} and str(relative)!='AGENTS.md'): raise PackError('Invalid definition path')
439
+ target = home / relative; target.parent.mkdir(parents=True,exist_ok=True)
440
+ data=content(definition['content'],relative.name in {'AGENTS.md','SKILL.md'} or (relative.parts[0]=='agents' and relative.suffix=='.md'))
441
+ if target.exists() and target.read_bytes()!=data:
442
+ if target in written_definitions and checkpoint_owns(written_definitions[target],manifest): continue
443
+ raise PackError('Conflicting skill/agent definition: ' + str(relative))
444
+ target.write_bytes(data); target.chmod(0o600)
445
+ written_definitions[target]=manifest
446
+ for tid,(_,thread) in thread_sources.items():
447
+ cwd = pathlib.Path(remap(thread['cwd'],mapping))
448
+ if not cwd.is_dir():
449
+ if any(cwd.is_relative_to(runtime / 'files') for _ in [0]): cwd.mkdir(parents=True,exist_ok=True)
450
+ else: raise PackError('External workspace unavailable; supply --map OLD=NEW: ' + str(cwd))
451
+ # Preserve original historical prose. Rebase typed current-context paths only.
452
+ for old, target in resource_roots.items():
453
+ path = pathlib.Path(target)
454
+ instructions = list(path.rglob('AGENTS.md')) if path.is_dir() else ([path] if path.name == 'AGENTS.md' else [])
455
+ for f in instructions:
456
+ if not f.is_relative_to(runtime): continue
457
+ text = f.read_text()
458
+ for source,dest in sorted(mapping.items(), key=lambda x:-len(x[0])): text = text.replace(source,dest)
459
+ f.write_text(text)
460
+ for f in [home/'AGENTS.md',*home.glob('skills/**/SKILL.md'),*home.glob('agents/**/*.md')]:
461
+ if not f.is_file(): continue
462
+ text=f.read_text()
463
+ for source,dest in sorted(mapping.items(),key=lambda x:-len(x[0])): text=text.replace(source,dest)
464
+ f.write_text(text)
465
+ offset_maps = {}; rollout_paths = {}; transforming = set(); donor_maps = {}
466
+ def parent_offsets(manifest,tid):
467
+ reference=manifest['threads'][tid]['rollout']
468
+ if reference==thread_sources[tid][1]['rollout']: return offset_maps[tid]
469
+ key=(reference['sha256'],tid)
470
+ if key not in donor_maps:
471
+ # The shared history was proved semantically identical above. Align its
472
+ # original record boundaries with the selected parent's new boundaries.
473
+ starts=[];position=0
474
+ with library.object(reference).open('rb') as stream:
475
+ for line in stream: starts.append(position);position+=len(line)
476
+ starts.append(position)
477
+ ends=list(offset_maps[tid].values())
478
+ if len(starts)!=len(ends): raise PackError('Shared history boundary mismatch')
479
+ donor_maps[key]=dict(zip(starts,ends))
480
+ return donor_maps[key]
481
+ def restore_rollout(tid):
482
+ if tid in offset_maps: return
483
+ if tid in transforming: raise PackError('Inherited history cycle')
484
+ transforming.add(tid)
485
+ manifest,thread = thread_sources[tid]
486
+ if thread.get('historyBase'): restore_rollout(thread['historyBase'])
487
+ src = library.object(thread['rollout'])
488
+ if pathlib.Path(thread['basename']).name != thread['basename']: raise PackError('Unsafe rollout basename')
489
+ folder = home / ('sessions' if tid in visible else 'archived_sessions')
490
+ date = re.match(r'rollout-(\d{4})-(\d{2})-(\d{2})T',thread['basename'])
491
+ if date: folder = folder.joinpath(*date.groups())
492
+ target = folder / thread['basename']
493
+ target.parent.mkdir(parents=True,exist_ok=True)
494
+ old_pos = 0; new_pos = 0; positions = {}
495
+ with src.open('rb') as source, target.open('wb') as dest:
496
+ for line in source:
497
+ positions[old_pos] = new_pos
498
+ obj = json.loads(line)
499
+ if manifest.get('startingContext',{}).get('kind')=='fresh':
500
+ # Only newly generated bootstrap context is rewritten. Historical
501
+ # work snapshots and later checkpointed conversations stay verbatim.
502
+ def rebind_bootstrap(value):
503
+ if isinstance(value,dict):return {k:rebind_bootstrap(v) for k,v in value.items()}
504
+ if isinstance(value,list):return [rebind_bootstrap(v) for v in value]
505
+ if isinstance(value,str):
506
+ for old,new in sorted(mapping.items(),key=lambda pair:-len(pair[0])):value=value.replace(old,new)
507
+ return value
508
+ obj=rebind_bootstrap(obj)
509
+ encoded=canonical(obj)+b'\n'
510
+ elif runtime_record(obj):
511
+ obj['payload'] = path_fields(obj.get('payload',{}),mapping)
512
+ base = obj['payload'].get('history_base')
513
+ if base and base.get('end_byte_offset') is not None:
514
+ parent_map = parent_offsets(manifest,base['thread_id'])
515
+ if base['end_byte_offset'] not in parent_map: raise PackError('Invalid inherited history boundary')
516
+ base['end_byte_offset'] = parent_map[base['end_byte_offset']]
517
+ encoded = canonical(obj) + b'\n'
518
+ else: encoded = line
519
+ dest.write(encoded); old_pos += len(line); new_pos += len(encoded)
520
+ positions[old_pos] = new_pos
521
+ target.chmod(0o600); offset_maps[tid] = positions; rollout_paths[tid] = str(target)
522
+ transforming.remove(tid)
523
+ for tid in thread_sources: restore_rollout(tid)
524
+ paused_goals = []; dbs = {}; row_keys = {}
525
+ try:
526
+ for manifest in manifests:
527
+ catalog = read_catalog(library,manifest)
528
+ for filename,data in catalog.items():
529
+ if filename not in DB_TABLES: raise PackError('Unknown database adapter')
530
+ if filename not in dbs:
531
+ conn = sqlite3.connect(home / filename); dbs[filename] = conn; row_keys[filename] = {}
532
+ for schema in data['schemas']:
533
+ if schema['table'] not in DB_TABLES[filename]: raise PackError('Unexpected native schema table')
534
+ prefix = r'^CREATE\s+' + ('TABLE' if schema['type']=='table' else r'(?:UNIQUE\s+)?INDEX') + r'\s+'
535
+ if not re.match(prefix,schema['sql'],re.I): raise PackError('Unsafe database schema')
536
+ conn.execute(schema['sql'])
537
+ else:
538
+ conn = dbs[filename]
539
+ existing = {r[0]:r[1] for r in conn.execute("SELECT name,sql FROM sqlite_master WHERE sql IS NOT NULL")}
540
+ if any(existing.get(s['name']) != s['sql'] for s in data['schemas']): raise PackError('Packs use incompatible Codex database schemas')
541
+ for table,entry in data['tables'].items():
542
+ if table not in DB_TABLES[filename]: raise PackError('Unexpected native table')
543
+ cols = entry['columns']
544
+ if cols != [r[1] for r in conn.execute('PRAGMA table_info('+quote(table)+')')]: raise PackError('Native schema columns mismatch')
545
+ primary = [r[1] for r in sorted(conn.execute('PRAGMA table_info('+quote(table)+')'),key=lambda r:r[5]) if r[5]]
546
+ rows_seen = row_keys[filename].setdefault(table,{})
547
+ for encoded in entry['rows']:
548
+ row = dict(zip(cols,[decode(v) for v in encoded]))
549
+ tid = row.get('thread_id') or (row.get('id') if table=='threads' else None)
550
+ if tid and tid not in thread_sources: continue
551
+ if tid and thread_sources[tid][0] is not manifest: continue
552
+ if table=='thread_spawn_edges' and (row['parent_thread_id'] not in thread_sources or row['child_thread_id'] not in thread_sources): continue
553
+ if table=='threads':
554
+ row['rollout_path'] = rollout_paths[tid]; row['cwd'] = remap(row['cwd'],mapping)
555
+ if row.get('agent_path'): row['agent_path']=remap(row['agent_path'],mapping)
556
+ row['archived'] = int(tid not in visible)
557
+ if tid not in visible and 'thread_section_id' in row: row['thread_section_id'] = None
558
+ if 'archived_at' in row and tid in visible: row['archived_at'] = None
559
+ if 'sandbox_policy' in row:
560
+ try: row['sandbox_policy'] = json.dumps(path_fields(json.loads(row['sandbox_policy']),mapping))
561
+ except (json.JSONDecodeError,TypeError): pass
562
+ if table in {'thread_turns','thread_history_projection_state'}:
563
+ for field in ['rollout_byte_offset','rollout_end_byte_offset','next_rollout_byte_offset']:
564
+ if row.get(field) is not None:
565
+ positions = offset_maps[tid]
566
+ if row[field] not in positions: raise PackError('Stored history offset has no matching rollout boundary')
567
+ row[field] = positions[row[field]]
568
+ if table=='thread_goals' and row['status']=='active': row['status']='paused'; paused_goals.append(tid)
569
+ if table=='project_roots':
570
+ row = path_fields(row,mapping)
571
+ if table=='thread_attachments':
572
+ try: row['payload']=json.dumps(path_fields(json.loads(row['payload']),mapping))
573
+ except (json.JSONDecodeError,TypeError): pass
574
+ row['identity_key']=remap(row['identity_key'],mapping)
575
+ key = tuple(row[c] for c in primary) if primary else tuple(str(row[c]) for c in cols)
576
+ if key in rows_seen:
577
+ if table=='_sqlx_migrations':
578
+ old = rows_seen[key]
579
+ if old.get('checksum') != row.get('checksum'): raise PackError('Migration version checksum mismatch')
580
+ elif rows_seen[key] != row: raise PackError('Conflicting native shared records in ' + table)
581
+ continue
582
+ rows_seen[key]=row
583
+ conn.execute('INSERT INTO '+quote(table)+' ('+','.join(quote(c) for c in cols)+') VALUES ('+','.join('?' for _ in cols)+')',[row[c] for c in cols])
584
+ for filename,conn in dbs.items():
585
+ conn.commit()
586
+ if conn.execute('PRAGMA integrity_check').fetchone()[0] != 'ok': raise PackError('Native database integrity failed')
587
+ finally:
588
+ for conn in dbs.values(): conn.close()
589
+ # Generate the dual desktop/server section mapping from the selected composition.
590
+ catalog_conn = sqlite3.connect(home / 'state_5.sqlite')
591
+ import uuid
592
+ groups = {}; pins = []; titles = {}; descriptions = {}; account_ids = set()
593
+ for agent in chosen:
594
+ groupkey = (agent['pack'],agent['set'])
595
+ if groupkey not in groups:
596
+ native_id = '01984de2-8f74-7c91-a3b2-5c5e937cf318' if agent.get('pinned') else str(uuid.uuid5(uuid.NAMESPACE_URL,'codex-pack-section:'+ '/'.join(groupkey)))
597
+ desktop_id = str(uuid.uuid5(uuid.NAMESPACE_URL,'codex-pack-desktop:'+ '/'.join(groupkey)))
598
+ groups[groupkey] = {'id':desktop_id,'name':agent['sectionName'],'hostSectionIds':{'local':native_id},'itemKeys':[],'appearance':None}
599
+ catalog_conn.execute('INSERT OR REPLACE INTO thread_sections (id,name) VALUES (?,?)',(native_id,agent['sectionName']))
600
+ g = groups[groupkey]; key = 'codex:thread:local:'+agent['threadId']; g['itemKeys'].append(key)
601
+ if agent.get('pinned'): pins.append(key)
602
+ titles[agent['threadId']] = agent['title']
603
+ catalog_conn.execute('UPDATE threads SET thread_section_id=?,section_position=? WHERE id=?',(g['hostSectionIds']['local'],agent.get('position',0),agent['threadId']))
604
+ catalog_conn.execute('DELETE FROM thread_sections WHERE id NOT IN (SELECT DISTINCT thread_section_id FROM threads WHERE thread_section_id IS NOT NULL)')
605
+ # The complete native catalog was restored above. Starting a legacy rollout
606
+ # backfill would overwrite captured timestamps and creator metadata from
607
+ # older, less complete session headers. Generate this target-local marker;
608
+ # never import another home's watermark or migration progress.
609
+ catalog_conn.execute('INSERT OR REPLACE INTO backfill_state (id,status,last_watermark,last_success_at,updated_at) VALUES (1,?,?,?,?)',
610
+ ('complete',None,int(time.time()),int(time.time())))
611
+ catalog_conn.commit(); catalog_conn.close()
612
+ for manifest in manifests:
613
+ account_ids.update(manifest['ui'].get('sourceAccountIds',[])); descriptions.update(manifest['ui'].get('descriptions',{}))
614
+ groups_list = [g for (_,sid),g in groups.items() if sid!='intakes']
615
+ sections = {account:{'sections':groups_list,'sectionOrder':['pinned']+[g['id'] for g in groups_list]+['chats','threads'],'collapsedSectionIds':[]} for account in account_ids}
616
+ gs={'thread-titles':{'titles':titles,'order':list(titles)},'pinned-thread-ids':[k.rsplit(':',1)[1] for k in pins],
617
+ 'thread-workspace-root-hints':{a['threadId']:remap(thread_sources[a['threadId']][1]['cwd'],mapping) for a in chosen},
618
+ 'electron-persisted-atom-state':{'sidebar-custom-sections-v3':sections,'unified-sidebar-pinned-order-v1':pins,'thread-descriptions-v1':descriptions,'flat-project-sidebar-preferences-v1':{'initialized':True,'mode':'project','chatSortMode':'updated_at','projectSortMode':'updated_at'}}}
619
+ atomic_json(home / '.codex-global-state.json',gs)
620
+ config_text = '[analytics]\nenabled = false\n'
621
+ for setting in ['model','model_reasoning_effort','personality']:
622
+ values={m.get('config',{}).get(setting) for m in manifests}-{None}
623
+ if len(values)==1: config_text=setting+' = '+json.dumps(next(iter(values)))+'\n'+config_text
624
+ (home/'config.toml').write_text(config_text); (home/'config.toml').chmod(0o600)
625
+ dependencies = {m['packId']:m.get('dependencies',{}) for m in manifests}
626
+ atomic_json(runtime/'dependencies.json',dependencies)
627
+ atomic_json(runtime/'bindings.json',mapping)
628
+ # Capturable semantic identities live independently of desktop caches.
629
+ declarations={'mcpServers':{},'plugins':[]}
630
+ for m in manifests:
631
+ for field in ['applications','capabilities','integrationNotes']:
632
+ declarations[field]=sorted(set(declarations.get(field,[])) | set(m.get('dependencies',{}).get(field,[])))
633
+ for key,value in m.get('dependencies',{}).get('mcpServers',{}).items():
634
+ if key in declarations['mcpServers'] and declarations['mcpServers'][key]!=value: raise PackError('Conflicting MCP prerequisite declarations: '+key)
635
+ declarations['mcpServers'][key]=value
636
+ declarations['plugins']=sorted(set(declarations['plugins']) | set(m.get('dependencies',{}).get('plugins',[])))
637
+ runtime_manifest={'agents':chosen,'resourcePaths':sorted(set(resource_roots.values())),
638
+ 'rootOrigins':{target:original for original,target in resource_roots.items()}, 'dependencies':declarations}
639
+ atomic_json(runtime/'agents.json',runtime_manifest)
640
+ from .topolo import attach_runtime
641
+ attach_runtime(library,runtime)
642
+ result={'threads':len(thread_sources),'agents':len(chosen),'historyOffsetsRebased':True,'activeGoalsPaused':sorted(set(paused_goals)),
643
+ 'home':str(home),'userData':str(runtime/'desktop-data'),'dependencies':dependencies,'mappings':mapping}
644
+ atomic_json(runtime/'verification.json',result)
645
+ return result