@topolo/cli 0.13.19 → 0.13.22
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/agent-packs/README.md +421 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +181 -16
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +103 -6
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +152 -73
- package/package.json +5 -5
|
@@ -275,7 +275,7 @@ def request(lib,operation,params=None):
|
|
|
275
275
|
record=read_json(lib.root/'live-bridge.json',{})
|
|
276
276
|
if not record: raise PackError('This desktop profile has no live bridge. Open it through topolo-agent-pack first.')
|
|
277
277
|
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
|
|
278
|
-
connection.settimeout(3 if operation=='status' else 240 if operation.startswith('lane-') else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
|
|
278
|
+
connection.settimeout(3 if operation=='status' else 240 if operation.startswith('lane-') or operation in {'work-submit','work-respond','work-re-review'} else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
|
|
279
279
|
try: connection.connect(record['socket'])
|
|
280
280
|
except OSError as exc: raise PackError('Managed live bridge is unavailable: '+str(exc)) from exc
|
|
281
281
|
connection.sendall(canonical({'token':record['token'],'operation':operation,**({'params':params} if params is not None else {})})+b'\n')
|
|
@@ -310,6 +310,7 @@ class BackendProxy:
|
|
|
310
310
|
self.token=secrets.token_hex(32);self.path=socket_path(lib)
|
|
311
311
|
self.execution=None;self.catalog_ready=asyncio.Event();self.catalog_id='topolo-catalog-'+uuid.uuid4().hex
|
|
312
312
|
self.catalog_rows=[];self.catalog_cursors=set()
|
|
313
|
+
self.work=None;self.work_startup_error=None;self.work_initialization=None
|
|
313
314
|
self.approvals=None;self.native_calls={};self.notifications=set()
|
|
314
315
|
self.permission_configuration=False
|
|
315
316
|
self.permission_batch=None
|
|
@@ -328,6 +329,15 @@ class BackendProxy:
|
|
|
328
329
|
if not active.get('runtime'): raise PackError('No selected managed runtime')
|
|
329
330
|
self.runtime=active['runtime'];env=os.environ.copy()
|
|
330
331
|
self.execution=load_gate(self.runtime);self.catalog_ready.clear()
|
|
332
|
+
self.work=None;self.work_startup_error=None;initialize_work=False
|
|
333
|
+
if (self.lib.root/'work/config.json').is_file():
|
|
334
|
+
if (self.lib.root/'work/cutover.json').exists() and not (self.lib.root/'work/cloud.json').exists():
|
|
335
|
+
# A closed legacy ledger cannot initialize a broker. Native policy
|
|
336
|
+
# and history reads remain available; model work stays fenced.
|
|
337
|
+
self.work_startup_error='Legacy task authority is fenced; attach this installation to the existing cloud authority before dispatch'
|
|
338
|
+
else:
|
|
339
|
+
initialize_work=True
|
|
340
|
+
self.work_startup_error='Task dispatcher initialization is pending; native task execution is unavailable'
|
|
331
341
|
self.approvals=ApprovalRouter(pathlib.Path(self.runtime)/'approval-inbox',
|
|
332
342
|
list(self.execution.roles.values()),uuid.uuid4().hex) if self.execution else None
|
|
333
343
|
self.catalog_rows=[];self.catalog_cursors=set()
|
|
@@ -335,6 +345,18 @@ class BackendProxy:
|
|
|
335
345
|
env['CODEX_HOME']=str(pathlib.Path(self.runtime)/'home');env['CODEX_CLI_PATH']=self.real
|
|
336
346
|
self.child=await asyncio.create_subprocess_exec(self.real,*self.args,env=env,stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,limit=64*1024*1024)
|
|
337
347
|
self.output_task=asyncio.create_task(self.output())
|
|
348
|
+
if initialize_work:self.work_initialization=asyncio.create_task(self.initialize_work())
|
|
349
|
+
|
|
350
|
+
async def initialize_work(self):
|
|
351
|
+
# Native organization-policy bootstrap must not depend on cloud latency.
|
|
352
|
+
# The explicit admission error remains until every broker check passes.
|
|
353
|
+
try:
|
|
354
|
+
from .work_native import Broker
|
|
355
|
+
broker=Broker(self);await broker.initialize()
|
|
356
|
+
self.work=broker;self.work_startup_error=None
|
|
357
|
+
if self.initialized:self.work.launch(self.work.resume_ready())
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
self.work=None;self.work_startup_error=str(exc) or 'Task dispatcher initialization failed'
|
|
338
360
|
|
|
339
361
|
async def output(self):
|
|
340
362
|
while line:=await self.child.stdout.readline():
|
|
@@ -362,15 +384,24 @@ class BackendProxy:
|
|
|
362
384
|
{**self.execution.status(),'protocol':self.protocol})
|
|
363
385
|
if 'id' in value and ('result' in value or 'error' in value):
|
|
364
386
|
self.pending.discard(str(value['id']))
|
|
365
|
-
if 'result' in value and isinstance(value['result'],dict) and 'userAgent' in value['result']:
|
|
387
|
+
if 'result' in value and isinstance(value['result'],dict) and 'userAgent' in value['result']:
|
|
388
|
+
self.initialized=True
|
|
389
|
+
if self.work:self.work.launch(self.work.resume_ready())
|
|
366
390
|
method=value.get('method');params=value.get('params') or {}
|
|
391
|
+
if method=='account/updated' and self.execution:
|
|
392
|
+
self.execution.catalog={};self.catalog_ready.clear()
|
|
367
393
|
if self.approvals:
|
|
368
394
|
record=self.approvals.capture(value)
|
|
369
395
|
if record:self.dynamic_permission_event(record)
|
|
370
396
|
if record and record['sourceThreadId']!=record['intakeThreadId'] and record['notificationState']!='delivered':
|
|
371
397
|
self.schedule_notification(record)
|
|
398
|
+
if method in {'turn/started','turn/completed','thread/status/changed'} and getattr(self,'work',None):
|
|
399
|
+
self.work.launch(self.work.notify())
|
|
400
|
+
if method=='item/completed' and getattr(self,'work',None):self.work.launch(self.work.progress(params))
|
|
372
401
|
if method=='turn/started': self.active_turns.add(params['threadId'])
|
|
373
402
|
if method=='turn/completed': self.active_turns.discard(params['threadId'])
|
|
403
|
+
if method=='turn/completed' and getattr(self,'work',None):
|
|
404
|
+
self.work.completed(params['threadId'],params['turn'])
|
|
374
405
|
if method=='thread/status/changed':
|
|
375
406
|
if params.get('status',{}).get('type')=='active':self.active_turns.add(params['threadId'])
|
|
376
407
|
elif params.get('status',{}).get('type') in {'idle','notLoaded','systemError'}:self.active_turns.discard(params['threadId'])
|
|
@@ -378,9 +409,20 @@ class BackendProxy:
|
|
|
378
409
|
for row in self.approvals.records():
|
|
379
410
|
if row['nativeConnectionCurrent'] and row['intakeThreadId']==params['threadId'] and row['notificationState']!='delivered':
|
|
380
411
|
self.schedule_notification(row)
|
|
412
|
+
if method in {'turn/started','turn/completed','thread/status/changed'} and getattr(self,'work',None):
|
|
413
|
+
from .coordinator_activity import record as record_activity
|
|
414
|
+
try:
|
|
415
|
+
record_activity(self.lib.root,{'runtime':self.runtime,'activeTurns':sorted(self.active_turns)})
|
|
416
|
+
self.activity_error=None
|
|
417
|
+
except Exception as exc:self.activity_error=str(exc)
|
|
381
418
|
if self.emit_native:sys.stdout.buffer.write(line);sys.stdout.buffer.flush()
|
|
382
419
|
|
|
383
420
|
async def stop_child(self):
|
|
421
|
+
if self.work_initialization:
|
|
422
|
+
self.work_initialization.cancel()
|
|
423
|
+
try:await self.work_initialization
|
|
424
|
+
except asyncio.CancelledError:pass
|
|
425
|
+
self.work_initialization=None
|
|
384
426
|
if self.child and self.child.returncode is None:
|
|
385
427
|
self.child.terminate()
|
|
386
428
|
try: await asyncio.wait_for(self.child.wait(),5)
|
|
@@ -390,13 +432,13 @@ class BackendProxy:
|
|
|
390
432
|
if self.output_task: await self.output_task
|
|
391
433
|
|
|
392
434
|
async def forward(self,value):
|
|
393
|
-
if self.smoke_busy and value.get('method') in {'thread/resume','turn/start'}:
|
|
435
|
+
if self.smoke_busy and value.get('method') in {'thread/resume','turn/start','turn/steer'}:
|
|
394
436
|
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,
|
|
395
437
|
'message':'Owned native smoke dispatch is being qualified'}})+b'\n');sys.stdout.buffer.flush();return
|
|
396
|
-
if self.permission_configuration and value.get('method') in {'thread/resume','turn/start'}:
|
|
438
|
+
if self.permission_configuration and value.get('method') in {'thread/resume','turn/start','turn/steer'}:
|
|
397
439
|
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':'Owned permission configuration in progress'}})+b'\n')
|
|
398
440
|
sys.stdout.buffer.flush();return
|
|
399
|
-
if value.get('method') in {'thread/resume','turn/start'} and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
|
|
441
|
+
if value.get('method') in {'thread/resume','turn/start','turn/steer'} and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
|
|
400
442
|
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,
|
|
401
443
|
'message':'Unresolved native permission operation requires reconciliation before execution'}})+b'\n')
|
|
402
444
|
sys.stdout.buffer.flush();return
|
|
@@ -404,11 +446,16 @@ class BackendProxy:
|
|
|
404
446
|
if 'id' in value:
|
|
405
447
|
sys.stdout.buffer.write(canonical({'id':value['id'],'error':{'code':-32000,'message':'Pack switch in progress; reconnecting backend'}})+b'\n');sys.stdout.buffer.flush()
|
|
406
448
|
return
|
|
407
|
-
if self.execution and value.get('method') in {'thread/resume','turn/start'} and (value.get('params') or {}).get('threadId') in self.execution.roles:
|
|
449
|
+
if self.execution and value.get('method') in {'thread/resume','turn/start','turn/steer'} and (value.get('params') or {}).get('threadId') in self.execution.roles:
|
|
408
450
|
try:
|
|
451
|
+
if value.get('method') in {'turn/start','turn/steer'} and self.work_startup_error:
|
|
452
|
+
raise PackError(self.work_startup_error)
|
|
409
453
|
await self.qualify_organization((value.get('params') or {})['threadId'])
|
|
410
|
-
await
|
|
454
|
+
await self.ensure_catalog()
|
|
455
|
+
value=await self.qualify_resume_path(value)
|
|
411
456
|
value=self.execution.prepare(value)
|
|
457
|
+
if value.get('method') in {'turn/start','turn/steer'} and getattr(self,'work',None):
|
|
458
|
+
value=await self.work.frontdoor(value)
|
|
412
459
|
except (PackError,asyncio.TimeoutError) as exc:
|
|
413
460
|
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':str(exc) or 'Native model catalog unavailable'}})+b'\n');sys.stdout.buffer.flush();return
|
|
414
461
|
if 'id' in value and 'method' in value:self.pending.add(str(value['id']))
|
|
@@ -429,6 +476,32 @@ class BackendProxy:
|
|
|
429
476
|
'params':{'includeHidden':True,**({'cursor':cursor} if cursor else {})}})+b'\n')
|
|
430
477
|
await self.child.stdin.drain()
|
|
431
478
|
|
|
479
|
+
async def ensure_catalog(self):
|
|
480
|
+
# Some desktop clients do not send the optional initialized notification
|
|
481
|
+
# that primes our private catalog request. Qualify on the owning native
|
|
482
|
+
# connection before resume instead of waiting for an event never sent.
|
|
483
|
+
if not self.catalog_ready.is_set():
|
|
484
|
+
result=await self.read_catalog()
|
|
485
|
+
self.execution.set_catalog(result)
|
|
486
|
+
self.catalog_ready.set()
|
|
487
|
+
# Preserve the native event-loop readiness fence: queued approvals or
|
|
488
|
+
# work notifications must be observed before a configuration effect.
|
|
489
|
+
await asyncio.wait_for(self.catalog_ready.wait(),10)
|
|
490
|
+
|
|
491
|
+
async def qualify_resume_path(self,value):
|
|
492
|
+
params=value.get('params') or {}
|
|
493
|
+
if value.get('method')!='thread/resume' or params.get('path') is None:return value
|
|
494
|
+
role=self.execution.roles[params['threadId']]
|
|
495
|
+
result=await self.call_native('thread/read',{'threadId':params['threadId'],'includeTurns':False})
|
|
496
|
+
thread=result.get('thread',{})
|
|
497
|
+
if (thread.get('id')!=params['threadId'] or thread.get('cwd')!=role['cwd'] or
|
|
498
|
+
not isinstance(thread.get('path'),str) or params['path']!=thread['path']):
|
|
499
|
+
raise PackError('Frontend resume path differs from the native owned thread')
|
|
500
|
+
# Resume by verified identity. Never allow a caller path to select a
|
|
501
|
+
# different history or change the reviewed execution settings.
|
|
502
|
+
value={**value,'params':{k:v for k,v in params.items() if k!='path'}}
|
|
503
|
+
return value
|
|
504
|
+
|
|
432
505
|
async def call_native(self,method,params,before_dispatch=None):
|
|
433
506
|
if self.managed_transport:
|
|
434
507
|
return await self.call_managed_native(method,params,before_dispatch)
|
|
@@ -438,7 +511,7 @@ class BackendProxy:
|
|
|
438
511
|
if not self.permission_configuration and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
|
|
439
512
|
raise PackError('Unresolved native permission operation requires reconciliation before execution')
|
|
440
513
|
await self.qualify_organization(params['threadId'])
|
|
441
|
-
await
|
|
514
|
+
await self.ensure_catalog()
|
|
442
515
|
if self.permission_configuration:
|
|
443
516
|
if self._permission_configuration_inputs is None:
|
|
444
517
|
raise PackError('No owned permission configuration snapshot; native settings were not sent')
|
|
@@ -621,7 +694,7 @@ class BackendProxy:
|
|
|
621
694
|
from . import topolo
|
|
622
695
|
consent=role['installationConsent']
|
|
623
696
|
agents=self.lib.load(consent['pack'])[0]['agents']
|
|
624
|
-
matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role.get('sourceRoleKey',role['key']) and a['threadId']==role.get('sourceRoleNativeId',role['threadId'])]
|
|
697
|
+
matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role.get('sourcePermissionRoleKey',role.get('sourceRoleKey',role['key'])) and a['threadId']==role.get('sourceRoleNativeId',role['threadId'])]
|
|
625
698
|
if len(matches)!=1:
|
|
626
699
|
raise PackError('Current consent has no unique live owned role')
|
|
627
700
|
effective_consent=consent
|
|
@@ -663,6 +736,9 @@ class BackendProxy:
|
|
|
663
736
|
declared={(r['key'],r['threadId']) for r in document['roles']}
|
|
664
737
|
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
665
738
|
native_ids=[r[0] for r in db.execute('SELECT id FROM threads')]
|
|
739
|
+
if getattr(self,'work',None):
|
|
740
|
+
task_ids=self.work.native_ids
|
|
741
|
+
native_ids=[tid for tid in native_ids if tid not in task_ids]
|
|
666
742
|
from .lanes import Registry
|
|
667
743
|
dynamic=[r for r in Registry(self.lib.root,'codex').records() if r['state']!='retired']
|
|
668
744
|
dynamic_ids={r['nativeId'] for r in dynamic}
|
|
@@ -1332,7 +1408,11 @@ class BackendProxy:
|
|
|
1332
1408
|
'nativeTransport':'existing-managed-window' if self.managed_transport else 'owned-stdio',
|
|
1333
1409
|
'managedTargetId':self.managed_transport.target_id if self.managed_transport else None,
|
|
1334
1410
|
'quiesced':self.quiesced,'initialized':self.initialized,
|
|
1335
|
-
'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),
|
|
1411
|
+
'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),'activityError':getattr(self,'activity_error',None),'workStartupError':getattr(self,'work_startup_error',None),
|
|
1412
|
+
'backendHealth':{'childReturnCode':getattr(self.child,'returncode',None),
|
|
1413
|
+
'responseReaderDone':self.output_task.done() if self.output_task else None,
|
|
1414
|
+
'catalogReady':self.catalog_ready.is_set(),
|
|
1415
|
+
'catalogModels':len(self.execution.catalog) if self.execution else 0},
|
|
1336
1416
|
'executionPolicy':self.execution.status() if self.execution else {'state':'not-configured'},
|
|
1337
1417
|
'executionProtocol':self.protocol if self.execution else None}
|
|
1338
1418
|
|
|
@@ -1364,8 +1444,10 @@ class BackendProxy:
|
|
|
1364
1444
|
raise PackError('Outgoing agents have active turns. Wait for their current turns; Codex can stay open.')
|
|
1365
1445
|
if self.pending:
|
|
1366
1446
|
raise PackError('Backend requests are in flight. Retry the switch; Codex can stay open.')
|
|
1367
|
-
self.
|
|
1368
|
-
|
|
1447
|
+
if getattr(self,'work',None):await self.work.quiesce()
|
|
1448
|
+
else:
|
|
1449
|
+
self.quiesced=True
|
|
1450
|
+
await self.stop_child()
|
|
1369
1451
|
elif op=='resume':
|
|
1370
1452
|
if self.quiesced:
|
|
1371
1453
|
raise PackError('Use the desktop reconnect action to resume a quiesced bridge')
|
|
@@ -1386,8 +1468,48 @@ class BackendProxy:
|
|
|
1386
1468
|
self.lane_close_event.set();result={'state':'normal-own-connection-close-requested'}
|
|
1387
1469
|
elif op in {'lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire'}:
|
|
1388
1470
|
result=await self.dynamic_lane(op,message.get('params') or {})
|
|
1471
|
+
elif op=='desktop-acceptance':
|
|
1472
|
+
from .desktop_acceptance import run
|
|
1473
|
+
result=await run(self,message.get('params') or {})
|
|
1389
1474
|
elif op=='owned-roster-read':
|
|
1390
1475
|
result=await self.owned_roster_read(message.get('params') or {})
|
|
1476
|
+
elif op.startswith('work-'):
|
|
1477
|
+
if not self.work:
|
|
1478
|
+
raise PackError(self.work_startup_error or 'Task dispatch is not installed on this owning connection')
|
|
1479
|
+
params=message.get('params') or {}
|
|
1480
|
+
if self.quiesced and op not in {'work-list','work-read','work-retained','work-watch','work-cutover-status'}:
|
|
1481
|
+
raise PackError('Native task admission is quiesced for cutover')
|
|
1482
|
+
if op=='work-cutover' and not params:
|
|
1483
|
+
result=await self.work.quiesce(cutover=True)
|
|
1484
|
+
elif op=='work-cutover-status' and not params:
|
|
1485
|
+
if not self.quiesced:raise PackError('Native cutover is not quiesced')
|
|
1486
|
+
from .cloud_cutover import closure
|
|
1487
|
+
if self.work.cutover_closure!=closure():raise PackError('Executing native cutover source changed')
|
|
1488
|
+
if self.work.cutover_record is None or read_json(self.work.store.root/'cutover.json')!=self.work.cutover_record:raise PackError('Native cutover acknowledgement changed')
|
|
1489
|
+
result=self.work.cutover_record
|
|
1490
|
+
elif op=='work-submit':
|
|
1491
|
+
result=await self.work.submit(params)
|
|
1492
|
+
elif op=='work-adoption-plan':
|
|
1493
|
+
result=await self.work.adoption_plan(params)
|
|
1494
|
+
elif op=='work-adopt':
|
|
1495
|
+
result=await self.work.adopt_retained(params)
|
|
1496
|
+
elif op=='work-respond':
|
|
1497
|
+
result=await self.work.respond(params)
|
|
1498
|
+
elif op=='work-re-review':
|
|
1499
|
+
result=await self.work.re_review(params)
|
|
1500
|
+
elif op=='work-list' and not params:
|
|
1501
|
+
result=await self.work.snapshot()
|
|
1502
|
+
elif op=='work-read' and set(params)=={'taskId'}:
|
|
1503
|
+
result=await self.work.read(params['taskId'])
|
|
1504
|
+
elif op=='work-retained':
|
|
1505
|
+
result=await self.work.state_call('retained_page',params)
|
|
1506
|
+
elif op=='work-reconcile-unstarted':
|
|
1507
|
+
result=await self.work.reconcile_unstarted(params)
|
|
1508
|
+
elif op=='work-reconcile-route':
|
|
1509
|
+
result=await self.work.reconcile_route(params)
|
|
1510
|
+
elif op=='work-watch' and not params:
|
|
1511
|
+
await self.work.watch(reader,writer);return
|
|
1512
|
+
else:raise PackError('Unsupported bounded work operation')
|
|
1391
1513
|
elif op in {'smoke-start','smoke-read'}:
|
|
1392
1514
|
result=await self.owned_smoke(op,message.get('params') or {})
|
|
1393
1515
|
elif op=='permissions-configure':
|
|
@@ -1419,7 +1541,7 @@ class BackendProxy:
|
|
|
1419
1541
|
result={'state':'native-response-forwarded','operationEffectVerified':False}
|
|
1420
1542
|
elif op!='status':
|
|
1421
1543
|
raise PackError('Unknown live bridge operation')
|
|
1422
|
-
if op not in {'lane-context-start','lane-context-read','lane-fixture-start','lane-fixture-read','lane-create','lane-close','lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire','approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','smoke-start','smoke-read'}:
|
|
1544
|
+
if not op.startswith('work-') and op not in {'lane-context-start','lane-context-read','lane-fixture-start','lane-fixture-read','lane-create','lane-close','lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire','approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','desktop-acceptance','smoke-start','smoke-read'}:
|
|
1423
1545
|
result=self.status()
|
|
1424
1546
|
except Exception as exc: result={'error':str(exc)}
|
|
1425
1547
|
writer.write(canonical(result)+b'\n');await writer.drain();writer.close();await writer.wait_closed()
|
|
@@ -1889,6 +2011,93 @@ def managed_lane_frontend(lib,runtime,planned,native_id=None,app='/Applications/
|
|
|
1889
2011
|
return {**result,'targetId':target,'selectedRuntime':str(root),'capability':capability,'proxyGeneration':generation}
|
|
1890
2012
|
finally:ws.close()
|
|
1891
2013
|
|
|
2014
|
+
def bind_sidebar(lib,runtime,app='/Applications/ChatGPT.app',presentation_journal=None,reconcile=None,reconnect_atoms=None):
|
|
2015
|
+
"""Existing managed manager, genuine account, guarded journaled presentation."""
|
|
2016
|
+
from . import native
|
|
2017
|
+
root=pathlib.Path(runtime);home=root/'home'
|
|
2018
|
+
generation=managed_backend_generation(lib,str(root))
|
|
2019
|
+
homes=[str(home)];layout=lib.root/'desktop-layout.json';stable=lib.root/'desktop-home'
|
|
2020
|
+
layout_hash=None
|
|
2021
|
+
if layout.exists():
|
|
2022
|
+
OwnedSmoke.owned(layout);OwnedSmoke.owned(stable,directory=True)
|
|
2023
|
+
if read_json(layout).get('owner') not in {'topolo-agent-pack','codex-pack'}:
|
|
2024
|
+
raise PackError('Stable sidebar layout owner differs')
|
|
2025
|
+
selected=lib.root/'selected-runtime'
|
|
2026
|
+
if not selected.is_symlink() or selected.resolve()!=root.resolve():
|
|
2027
|
+
raise PackError('Stable sidebar selected runtime differs')
|
|
2028
|
+
for name in ['config.toml','state_5.sqlite','sessions']:
|
|
2029
|
+
target=stable/name
|
|
2030
|
+
if not target.is_symlink() or os.readlink(target)!=str(pathlib.Path('..')/'selected-runtime'/'home'/name):
|
|
2031
|
+
raise PackError('Stable sidebar execution route differs')
|
|
2032
|
+
homes.append(str(stable));layout_hash=hashlib.sha256(layout.read_bytes()).hexdigest()
|
|
2033
|
+
capability=adapter(app);ws,_=renderer(lib)
|
|
2034
|
+
try:
|
|
2035
|
+
params={'adapter':capability,'homes':homes,'configFile':str(home/'config.toml'),'cwd':str(home)}
|
|
2036
|
+
expression="""(async()=>{
|
|
2037
|
+
const p=PARAMS,services=(await import(p.adapter.module))[p.adapter.export];
|
|
2038
|
+
const opened=await services.appServerManagers.open('local');
|
|
2039
|
+
if(opened?.status!=='ready'||!opened.manager)throw new Error('Existing managed backend unavailable');
|
|
2040
|
+
const manager=opened.manager,home=await manager.getCodexHome();
|
|
2041
|
+
if(typeof home!=='string'||!p.homes.includes(home))throw new Error('Selected sidebar home changed');
|
|
2042
|
+
const config=await manager.sendRequest('config/read',{cwd:p.cwd,includeLayers:true});
|
|
2043
|
+
const origins=config?.layers?.filter(layer=>layer.name?.type==='user'&&!layer.disabledReason);
|
|
2044
|
+
if(!Array.isArray(origins)||!origins.length||origins.some(layer=>layer.name.file!==p.configFile||typeof layer.version!=='string'||!layer.version))throw new Error('Selected sidebar config provenance changed');
|
|
2045
|
+
const a=await manager.sendRequest('account/read',{refreshToken:false});
|
|
2046
|
+
if(await manager.getCodexHome()!==home)throw new Error('Selected sidebar home changed');
|
|
2047
|
+
return {requiresOpenaiAuth:a.requiresOpenaiAuth,account:a.account?{type:a.account.type}:null,
|
|
2048
|
+
workspaceRouting:a.workspaceRouting?{chatgptAccountId:a.workspaceRouting.chatgptAccountId}:null,
|
|
2049
|
+
_nativeContext:{home,configOrigins:origins.map(layer=>({name:layer.name,version:layer.version}))}};
|
|
2050
|
+
})()""".replace('PARAMS',json.dumps(params),1)
|
|
2051
|
+
account=ws.evaluate(expression);binding=native.sidebar_binding(root,account)
|
|
2052
|
+
origin=account.get('_nativeContext') if isinstance(account,dict) else None
|
|
2053
|
+
if not isinstance(origin,dict) or origin.get('home') not in homes or not origin.get('configOrigins'):
|
|
2054
|
+
raise PackError('Actual managed sidebar provenance missing')
|
|
2055
|
+
context={'kind':'managed','app':app,'home':origin['home'],'configOrigins':origin['configOrigins'],
|
|
2056
|
+
'account':{'type':(account.get('account') or {}).get('type'),'accountId':binding['accountId'],
|
|
2057
|
+
'requiresOpenaiAuth':account['requiresOpenaiAuth']},'generation':generation,'layoutSHA256':layout_hash}
|
|
2058
|
+
def guard():
|
|
2059
|
+
if ws.evaluate(expression)!=account:raise PackError('Selected sidebar account/home/config changed')
|
|
2060
|
+
if managed_backend_generation(lib,str(root))!=generation:raise PackError('Selected sidebar backend changed')
|
|
2061
|
+
if layout_hash is not None:
|
|
2062
|
+
if hashlib.sha256(layout.read_bytes()).hexdigest()!=layout_hash or (lib.root/'selected-runtime').resolve()!=root.resolve():
|
|
2063
|
+
raise PackError('Stable sidebar route/layout changed')
|
|
2064
|
+
for name in ['config.toml','state_5.sqlite','sessions']:
|
|
2065
|
+
target=stable/name
|
|
2066
|
+
if not target.is_symlink() or os.readlink(target)!=str(pathlib.Path('..')/'selected-runtime'/'home'/name):
|
|
2067
|
+
raise PackError('Stable sidebar route changed')
|
|
2068
|
+
return context
|
|
2069
|
+
guard()
|
|
2070
|
+
current=fetch(ws,'get-global-state',{'key':'electron-persisted-atom-state'}).get('value') or {}
|
|
2071
|
+
if reconcile is not None:
|
|
2072
|
+
path,document=reconcile
|
|
2073
|
+
return native.reconcile_sidebar(lib,path,document,guard(),current,guard)
|
|
2074
|
+
sections=dict(current.get('sidebar-custom-sections-v3',{}));sections.update(binding['sections'])
|
|
2075
|
+
# set-global-state replaces the whole value for its key. Reconnect must
|
|
2076
|
+
# not send a filtered container early: that would erase live account
|
|
2077
|
+
# presentation. Merge ordinary runtime atoms only inside this guarded,
|
|
2078
|
+
# journaled write; destination sections/pins come exclusively from the
|
|
2079
|
+
# fresh managed account, never the captured/offline runtime account.
|
|
2080
|
+
ordinary={k:v for k,v in (reconnect_atoms or {}).items()
|
|
2081
|
+
if k not in {'sidebar-custom-sections-v3','unified-sidebar-pinned-order-v1'}}
|
|
2082
|
+
value={**current,**ordinary,'sidebar-custom-sections-v3':sections,'unified-sidebar-pinned-order-v1':binding['pins']}
|
|
2083
|
+
report={k:v for k,v in binding.items() if k not in {'sections','pins'}}
|
|
2084
|
+
# There is no qualified identity-bound/CAS setter here. Journal the
|
|
2085
|
+
# unavoidable mid-dispatch uncertainty rather than claiming atomicity.
|
|
2086
|
+
mutation=native.SidebarJournal(lib,root,context,current,value,report,parent=presentation_journal)
|
|
2087
|
+
try:
|
|
2088
|
+
guard() # AFTER awaited settings read and durable intent, BEFORE setter.
|
|
2089
|
+
mutation.dispatched=True
|
|
2090
|
+
if not fetch(ws,'set-global-state',{'key':'electron-persisted-atom-state','value':value}).get('success'):
|
|
2091
|
+
raise PackError('Desktop refused sidebar binding')
|
|
2092
|
+
actual=fetch(ws,'get-global-state',{'key':'electron-persisted-atom-state'}).get('value')
|
|
2093
|
+
if actual!=value:raise PackError('Desktop sidebar read-back differs')
|
|
2094
|
+
guard();atomic_json(root/'sidebar-binding.json',report)
|
|
2095
|
+
if read_json(root/'sidebar-binding.json')!=report:raise PackError('Sidebar report read-back differs')
|
|
2096
|
+
guard();mutation.complete();return report
|
|
2097
|
+
except BaseException as error:
|
|
2098
|
+
mutation.failed(error);raise
|
|
2099
|
+
finally:ws.close()
|
|
2100
|
+
|
|
1892
2101
|
def checkpoint_ui(lib,runtime):
|
|
1893
2102
|
ws,_=renderer(lib)
|
|
1894
2103
|
try:
|
|
@@ -1907,15 +2116,26 @@ def reconcile(ws,capability,incoming,outgoing):
|
|
|
1907
2116
|
params=json.dumps({'adapter':capability,'incoming':incoming,'outgoing':outgoing})
|
|
1908
2117
|
return ws.evaluate('''(async()=>{const p=PARAMS;const c=(await import(p.adapter.module))[p.adapter.export]?.localThreadCatalog;if(!c)throw new Error('Catalog capability unavailable');for(const threadId of p.outgoing)await c.removeMissingEntry({hostId:'local',threadId});for(const threadId of p.incoming)await c.notifyThread({hostId:'local',threadId},'upsert');await c.invalidateSource('local');await c.requestSync(['local'],'immediate');let proof;const deadline=Date.now()+15000;do{const entries=await c.readEntries(p.incoming.map(threadId=>({hostId:'local',threadId})));const missing=await c.readEntries(p.outgoing.map(threadId=>({hostId:'local',threadId})));proof={incoming:entries.map(e=>e.threadId),titles:entries.map(e=>e.displayTitle),outgoing:missing.map(e=>e.threadId),count:await c.readThreadCount('local')};if(proof.incoming.length===p.incoming.length&&!proof.outgoing.length&&proof.count===p.incoming.length)return proof;await new Promise(r=>setTimeout(r,50))}while(Date.now()<deadline);return proof})()'''.replace('PARAMS',params,1))
|
|
1909
2118
|
|
|
1910
|
-
def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app'):
|
|
2119
|
+
def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app',presentation_journal=None):
|
|
1911
2120
|
ws,target_id=renderer(lib)
|
|
1912
2121
|
try:
|
|
2122
|
+
if presentation_journal is None:
|
|
2123
|
+
raise PackError('Reconnect presentation requires its original transaction journal')
|
|
2124
|
+
jpath,jdocument=presentation_journal
|
|
2125
|
+
targets={jdocument.get('targetRuntime'),jdocument.get('before',{}).get('runtime'),jdocument.get('after',{}).get('runtime')}
|
|
2126
|
+
if pathlib.Path(jpath).parent!=lib.root/'journals' or read_json(jpath)!=jdocument or str(runtime) not in targets or jdocument.get('phase') in {'complete','rolled-back'}:
|
|
2127
|
+
raise PackError('Reconnect presentation transaction changed')
|
|
2128
|
+
if jdocument.get('sidebar',{}).get('state') in {'UNKNOWN','may-have-effects'}:
|
|
2129
|
+
raise PackError('Pending sidebar effects require original owner reconciliation')
|
|
1913
2130
|
current_bridge=available(lib)
|
|
1914
2131
|
if current_bridge and not current_bridge['quiesced']:quiesce(lib)
|
|
1915
2132
|
capability=adapter(app);bind(lib,runtime)
|
|
1916
2133
|
gs=read_json(pathlib.Path(runtime)/'home'/'.codex-global-state.json')
|
|
1917
|
-
|
|
1918
|
-
|
|
2134
|
+
# Defer the entire atom container. The native API replaces a key's
|
|
2135
|
+
# value; removing nested presentation keys here would still erase them.
|
|
2136
|
+
# Its ordinary atoms join the post-restart guarded binder below.
|
|
2137
|
+
runtime_atoms=gs.get('electron-persisted-atom-state',{})
|
|
2138
|
+
updates={key:value for key,value in gs.items() if key!='electron-persisted-atom-state'}
|
|
1919
2139
|
for key,value in updates.items():
|
|
1920
2140
|
result=fetch(ws,'set-global-state',{'key':key,'value':value})
|
|
1921
2141
|
if not result.get('success'): raise PackError('Desktop refused state update: '+key)
|
|
@@ -1927,6 +2147,7 @@ def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app')
|
|
|
1927
2147
|
if bridge and not bridge['quiesced'] and bridge['runtime']==str(runtime) and bridge['initialized']:break
|
|
1928
2148
|
time.sleep(0.05)
|
|
1929
2149
|
else:raise PackError('Managed backend did not reconnect; run topolo-agent-pack recover')
|
|
2150
|
+
sidebar_binding=bind_sidebar(lib,runtime,app,presentation_journal=presentation_journal,reconnect_atoms=runtime_atoms)
|
|
1930
2151
|
proof=reconcile(ws,capability,incoming,outgoing)
|
|
1931
2152
|
if set(proof['incoming'])!=set(incoming) or proof['outgoing'] or proof['count']!=len(incoming):raise PackError('Sidebar catalog did not match selected runtime; run recover')
|
|
1932
2153
|
# Soft reload clears thread/composer caches while retaining this exact
|
|
@@ -1949,9 +2170,42 @@ def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app')
|
|
|
1949
2170
|
ws.close();time.sleep(0.05)
|
|
1950
2171
|
except (OSError,PackError):time.sleep(0.05)
|
|
1951
2172
|
else:raise PackError('Desktop did not render after reconnect; run recover')
|
|
1952
|
-
return {'state':'same-window-verified','app':str(app),'targetId':target_id,'desktopPid':bridge['desktopPid'],'frontendVerified':True,'windowClosed':False,'catalog':proof,'renderedTitles':sidebar,'renderedThreadIds':sorted(shown),'selectedThreadId':selected}
|
|
2173
|
+
return {'state':'same-window-verified','app':str(app),'targetId':target_id,'desktopPid':bridge['desktopPid'],'frontendVerified':True,'windowClosed':False,'catalog':proof,'renderedTitles':sidebar,'renderedThreadIds':sorted(shown),'selectedThreadId':selected,'sidebarBinding':sidebar_binding,'sectionsRenderedVerified':False}
|
|
1953
2174
|
finally:ws.close()
|
|
1954
2175
|
|
|
2176
|
+
def reconcile_desktop_catalog(lib,home,runtime):
|
|
2177
|
+
"""Drop obsolete derived local catalog rows before an offline cold launch.
|
|
2178
|
+
|
|
2179
|
+
Native transcripts and the timeline ledger are never changed. A complete
|
|
2180
|
+
SQLite backup makes cache repair recoverable; cloud/remote hosts stay intact.
|
|
2181
|
+
"""
|
|
2182
|
+
from . import native
|
|
2183
|
+
path=home/'sqlite/codex-dev.db'
|
|
2184
|
+
if not path.exists():return
|
|
2185
|
+
if path.is_symlink() or path.stat().st_uid!=os.getuid():
|
|
2186
|
+
raise PackError('Unowned desktop catalog')
|
|
2187
|
+
native.assert_idle(home)
|
|
2188
|
+
state=pathlib.Path(runtime)/'home/state_5.sqlite'
|
|
2189
|
+
with closing(sqlite3.connect('file:'+str(state)+'?mode=ro',uri=True)) as db:
|
|
2190
|
+
active={r[0] for r in db.execute('SELECT id FROM threads WHERE archived=0')}
|
|
2191
|
+
with closing(sqlite3.connect(path)) as db:
|
|
2192
|
+
tables={r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
2193
|
+
if 'local_thread_catalog' not in tables:
|
|
2194
|
+
return
|
|
2195
|
+
stale=[r[0] for r in db.execute("SELECT thread_id FROM local_thread_catalog WHERE host_id='local'") if r[0] not in active]
|
|
2196
|
+
if not stale:
|
|
2197
|
+
return
|
|
2198
|
+
folder=lib.root/'catalog-recovery';folder.mkdir(exist_ok=True,mode=0o700)
|
|
2199
|
+
backup=folder/(uuid.uuid4().hex+'.sqlite')
|
|
2200
|
+
with closing(sqlite3.connect(backup)) as saved:db.backup(saved)
|
|
2201
|
+
backup.chmod(0o600)
|
|
2202
|
+
with db:
|
|
2203
|
+
db.executemany("DELETE FROM local_thread_catalog WHERE host_id='local' AND thread_id=?",[(v,) for v in stale])
|
|
2204
|
+
for table in ['local_thread_catalog_sync_state','local_thread_catalog_scan_checkpoints','local_thread_catalog_scan_entries']:
|
|
2205
|
+
if table in tables:db.execute('DELETE FROM '+table+" WHERE host_id='local'")
|
|
2206
|
+
if 'local_thread_catalog_metadata' in tables:db.execute('UPDATE local_thread_catalog_metadata SET catalog_revision=catalog_revision+1 WHERE id=1')
|
|
2207
|
+
atomic_json(backup.with_suffix('.json'),{'boundary':'derived desktop catalog only','runtime':str(runtime),'removedIds':stale,'retainedActiveIds':sorted(active),'backup':str(backup),'nativeHistoryChanged':False})
|
|
2208
|
+
|
|
1955
2209
|
def prepare(lib,real):
|
|
1956
2210
|
home=lib.root/'desktop-home'
|
|
1957
2211
|
if home.exists() and not (lib.root/'desktop-layout.json').exists():raise PackError('Existing desktop-home is unowned; choose a separate pack library')
|
|
@@ -1962,7 +2216,20 @@ def prepare(lib,real):
|
|
|
1962
2216
|
home.mkdir(exist_ok=True,mode=0o700)
|
|
1963
2217
|
atomic_json(lib.root/'desktop-layout.json',{'owner':'topolo-agent-pack','schemaVersion':1})
|
|
1964
2218
|
bind(lib,lib.active()['runtime'])
|
|
1965
|
-
|
|
2219
|
+
reconcile_desktop_catalog(lib,home,lib.active()['runtime'])
|
|
2220
|
+
# The control home outlives a selected runtime. Its cached sidebar must
|
|
2221
|
+
# follow that runtime on every cold launch, rather than only its first one.
|
|
2222
|
+
state_path=home/'.codex-global-state.json'
|
|
2223
|
+
current=read_json(state_path,{})
|
|
2224
|
+
runtime=str(lib.active()['runtime'])
|
|
2225
|
+
if current.get('topolo-selected-runtime')==runtime:return executable,home
|
|
2226
|
+
selected=read_json(pathlib.Path(lib.active()['runtime'])/'home/.codex-global-state.json')
|
|
2227
|
+
for key in ['pinned-thread-ids','thread-titles','thread-workspace-root-hints']:
|
|
2228
|
+
current[key]=selected.get(key,{} if key!='pinned-thread-ids' else [])
|
|
2229
|
+
current['electron-persisted-atom-state']={**current.get('electron-persisted-atom-state',{}),**selected.get('electron-persisted-atom-state',{})}
|
|
2230
|
+
current.pop('app-server-migrated-pinned-thread-ids-by-host',None)
|
|
2231
|
+
current['topolo-selected-runtime']=runtime
|
|
2232
|
+
atomic_json(state_path,current)
|
|
1966
2233
|
return executable,home
|
|
1967
2234
|
|
|
1968
2235
|
def bind(lib,runtime):
|