aline-ai 0.6.6__py3-none-any.whl → 0.6.7__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,11 +1,11 @@
1
- aline_ai-0.6.6.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=0jG4gYJd6Zmf7o7WaFAwx0d2VJ0amHPKJ8u3BqkhG6A,1623
3
- realign/agent_names.py,sha256=ml6Jc1B_nRRr-FcW1tIFMyqxw-90RBLcfzj6fwJRB4U,1158
1
+ aline_ai-0.6.7.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=_XHUZdw-wtPtpgm845liUkFT-VXg86ZiqDK3pdnnh9Q,1623
3
+ realign/agent_names.py,sha256=H4oVJMkqg1ZYCk58vD_Jh9apaAHSFJRswa-C9SPdJxc,1171
4
4
  realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
5
5
  realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
6
6
  realign/cli.py,sha256=IctmQ0OTb6kLlWRFRQumdhY6-CpcpFtocdc68KiwxvM,37748
7
- realign/codex_detector.py,sha256=t_34CkvxP4x9CxuHmbk8YhEhu8RKLRG-CDr_FaRMQYY,5536
8
- realign/codex_home.py,sha256=9cfO_kF-WYKAJXdSnceTB1PaRPBpZjAWyDtO0_U_BvA,3569
7
+ realign/codex_detector.py,sha256=WGIClvlrFVCqJ5vR9DrKVsp1eJhOShvcaXibTHb0Nfc,6304
8
+ realign/codex_home.py,sha256=ljkW8uCfQD4cisEJtPNQmIgaR0yEfWSyHwoVQFY-6p4,4374
9
9
  realign/codex_terminal_linker.py,sha256=L2Ha4drlZ7Sbq2jzXyxczOdUY3S5fu1gJqoI5WN9CKk,6211
10
10
  realign/config.py,sha256=Znfs43AjiK90LGWnArDPWyrE859sdZQAPIb0KAcU3Ig,9252
11
11
  realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
@@ -16,9 +16,9 @@ realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,487
16
16
  realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
17
17
  realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
18
18
  realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
19
- realign/watcher_core.py,sha256=CeHY6PN8vzn4cNezjCWNr3yLEuNsRsshfI5W3HfsXQk,110978
19
+ realign/watcher_core.py,sha256=XOJarc_jjlf51Gj8ytcdEeaDUkVIq3Ow0bMbFHbKfAM,116690
20
20
  realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
21
- realign/worker_core.py,sha256=TrFvqlElpa3Vnz7yumVfYaw4h9GcOLhuLhumPkeNCTg,11195
21
+ realign/worker_core.py,sha256=IXDFvkmeboOUvWyNJ3iZ7xlfxAulPnmFlAtuuJSdgRo,12362
22
22
  realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
23
23
  realign/adapters/__init__.py,sha256=alkJr7DRn_CrJecSJRjRJOHHnkz9EnZ5TnsU8n1Bb0k,719
24
24
  realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
@@ -31,7 +31,7 @@ realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFr
31
31
  realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
32
32
  realign/claude_hooks/stop_hook.py,sha256=Bzf6CjHQ-0q61SrDrpIvcwt_BmDO1FE-f8cws_aA-Is,13582
33
33
  realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
34
- realign/claude_hooks/terminal_state.py,sha256=Ywl173lD9-eUTXvYFaCczkQXNIqzzKL1zXAJPNpMiW8,5728
34
+ realign/claude_hooks/terminal_state.py,sha256=2ygTbVnh2b59vRLuN-TyWcXR94NKFlaVwOhS3ipqn58,6647
35
35
  realign/claude_hooks/user_prompt_submit_hook.py,sha256=8e0zNonT95TH2uuISYp3am_RD7c84Ghh1WRPgs023DI,10625
36
36
  realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
37
37
  realign/commands/__init__.py,sha256=WVaVT1orM2Z0PYaG3X6tkKb_t2v3n_3siCadh1qd_QA,107
@@ -40,12 +40,13 @@ realign/commands/agent.py,sha256=3CS48bMn7tkdDWKRrfg7CYbhcJK4Pz40YjYMvwD7c2w,317
40
40
  realign/commands/auth.py,sha256=QrPukpP-ogYEDSwztV0NOYI-HDgn5fPxlCQ1-e2n7gU,11082
41
41
  realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
42
42
  realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
43
- realign/commands/doctor.py,sha256=q5UOrUR5Uai4AxgaeOnK1Hig5I5UX7m3Vt00tPnUllg,18289
44
- realign/commands/export_shares.py,sha256=oQHKSBQKr0PmyjuZibBdO8bmZ1arODRNclws3U9Z0Js,142253
45
- realign/commands/import_shares.py,sha256=HiswLlYHqR0dR3wgB7Rs54_WownqahIs5IdyJOHuot8,25572
43
+ realign/commands/doctor.py,sha256=0c1TZuA_cw1CSU0yKMVRU-18uTxdqjXKJ8lP2CTTNSQ,20656
44
+ realign/commands/export_shares.py,sha256=b8dpVBx2HkbHVk9pSFXnErlAr0umciAOPpuxvTJyOBI,148467
45
+ realign/commands/import_shares.py,sha256=qAH007WCQ6bwWP09MEJVmgJlRC8c-QicB2HYvMBqyRM,32966
46
46
  realign/commands/init.py,sha256=6rBr1LVIrQLbUH_UvoDhkF1qXmMh2xkjNWCYAUz5Tho,35274
47
47
  realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
48
48
  realign/commands/search.py,sha256=QlUDzRDD6ebq21LTtLe5-OZM62iwDrDqfbnXbuxfklU,27516
49
+ realign/commands/sync_agent.py,sha256=XRcHN00TjfzGwTw3O_OXqb9Yj0lMFfDX0S7oizVpS6E,12454
49
50
  realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
50
51
  realign/commands/watcher.py,sha256=4WTThIgr-Z5guKh_JqGDcPmerr97XiHrVaaijmckHsA,134350
51
52
  realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
@@ -54,38 +55,37 @@ realign/dashboard/app.py,sha256=e257euP0gR9nA0w1susuLkG9tnYQk1IJJdgAICnIYxs,1039
54
55
  realign/dashboard/clipboard.py,sha256=81frq83E_urqLkwuCvtl0hiTEjavtdQn8kCi72jJWcs,1207
55
56
  realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
56
57
  realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
57
- realign/dashboard/tmux_manager.py,sha256=Fc6OQbnOO4YV47BnrIkcr0SHnQuSFwUSqhepNkpqKLs,32942
58
+ realign/dashboard/tmux_manager.py,sha256=sS6fo7UVPHWxYm1RYtLDPmwsagFh5RO6TRwYd1CuHaI,34581
58
59
  realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
59
60
  realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
60
61
  realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
61
62
  realign/dashboard/screens/__init__.py,sha256=MiefFamCYRrzTwQXiCUdybaJaFxlK5XKtLHaSQmqDv0,597
62
63
  realign/dashboard/screens/agent_detail.py,sha256=N-iUC4434C91OcDu4dkQaxS_NXQ5Yl5sqNBb2mTmoBw,10490
63
64
  realign/dashboard/screens/create_agent.py,sha256=06uiQYvz-Xvn4Xm689o3tdhzb2HQ0gdzAA1WHVEwziM,11706
64
- realign/dashboard/screens/create_agent_info.py,sha256=B5rGTb5WPREZPbfopQaXZviXz0lZZHV9l0gmNyGhNII,3914
65
+ realign/dashboard/screens/create_agent_info.py,sha256=K2Rbp4zHVdanPT3Fp82We4qlSAM-0IBZXPLuQuevuME,7838
65
66
  realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
66
67
  realign/dashboard/screens/event_detail.py,sha256=-pqt3NBoeTXGJKtbndZy-msklwXTeNWMS4H12oMG5ks,20175
67
68
  realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
68
69
  realign/dashboard/screens/session_detail.py,sha256=TBkHqSHyMxsLB2QdZq9m1EoiH8oRVDbPrjt-a8I9sHs,9561
69
70
  realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
70
- realign/dashboard/styles/dashboard.tcss,sha256=AhYhvm1hBZee0Gzv0C8qsx_-6DqLw4Gg-h_XiOGQHJ0,4557
71
- realign/dashboard/widgets/__init__.py,sha256=1FBQzar5Jd4hBZtANVKb_dNxFAqVqrwJJkBpeLmCq-Q,640
72
- realign/dashboard/widgets/agents_panel.py,sha256=GxnOrg6C1dPw0lhOfrjIHJAk04MMwhYJwMUY1SfMuCs,38875
71
+ realign/dashboard/styles/dashboard.tcss,sha256=9W5Tx0lgyGb4HU-z-Kn7gBdexIK0aPe0bkVn2k_AseM,3288
72
+ realign/dashboard/widgets/__init__.py,sha256=33qjCa6WCQ7XojRiStdR73jX2xpKV_RlBqodVDQWkxs,577
73
+ realign/dashboard/widgets/agents_panel.py,sha256=TtOX9RlF0CuwRTe1sXoo1xaf7ZykJA-YFmMu0-SKe2g,43299
73
74
  realign/dashboard/widgets/config_panel.py,sha256=eRJRuqImQ8eJIKCEj4O8EvYxI-ht_anrcYbT5JskWyU,15972
74
75
  realign/dashboard/widgets/events_table.py,sha256=0cMvE0KdZFBZyvywv7vlt005qsR0aLQnQiMf3ZzK7RY,30218
75
76
  realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
76
77
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
77
78
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
78
79
  realign/dashboard/widgets/sessions_table.py,sha256=6y78pEkyAmNsU4_o46PbwXRFW17fc5khgheBi4LjBNg,33374
79
- realign/dashboard/widgets/terminal_panel.py,sha256=at8whXa8Bsn_icbyerHG21tb2BsnQikAMlf4NfIpTGw,61504
80
80
  realign/dashboard/widgets/watcher_panel.py,sha256=emVY1-aot9Dnf5UI9yyNeEmp4d2Gb-lrC28DjkeLjKA,19575
81
81
  realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
82
82
  realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
83
- realign/db/base.py,sha256=XIW25zh8UD2AC4zJaGXWmRkcZxk59nVwqDFysk_vfZw,14370
83
+ realign/db/base.py,sha256=ShufW-c0ntKYsTWCbiXJ5W-G_H_mWN4YlnUuspWWu34,15589
84
84
  realign/db/locks.py,sha256=dUQu9Yo5nZstMSPXZPYzN0xqX8UXhJgNV_PmYEJ-rK0,1801
85
85
  realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
86
86
  realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
87
- realign/db/schema.py,sha256=uOp8B7-zq5nsdyw5aNpwhhAr5lnoYAWNES6Mr1GCh7Y,31803
88
- realign/db/sqlite_db.py,sha256=Ixcym-aUtVkgpav0FQ3whCp6PblwBtMGrHmvK8GHvMY,113707
87
+ realign/db/schema.py,sha256=IWPbeDYrbC1eZGQAy8k1rk0r2NnABJzXSSg8bb00XBw,33885
88
+ realign/db/sqlite_db.py,sha256=u4yybbXzOApYPnHkHlR59qBSyWPoIqgRppTB4ht5taM,119736
89
89
  realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
90
90
  realign/events/agent_summarizer.py,sha256=vh65tYgo1NOYsIpVPR253nnOr-MIejC4KG5dGvDzKv4,5413
91
91
  realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
@@ -104,8 +104,8 @@ realign/triggers/next_turn_trigger.py,sha256=-x80_I-WmIjXXzQHEPBykgx_GQW6oKaLDQx
104
104
  realign/triggers/registry.py,sha256=dkIjSd8Bg-hF0nxaO2Fi2K-0Zipqv6vVjc-HYSrA_fY,3656
105
105
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
106
106
  realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
107
- aline_ai-0.6.6.dist-info/METADATA,sha256=_RwRrqtSa3O_Rt0sXdK18VX8fHbYtz9gtE-tkVfGe5w,1597
108
- aline_ai-0.6.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
109
- aline_ai-0.6.6.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
110
- aline_ai-0.6.6.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
111
- aline_ai-0.6.6.dist-info/RECORD,,
107
+ aline_ai-0.6.7.dist-info/METADATA,sha256=GyI08kzWpN5QDEsgynocnlk8Cp5zSTiKXRltp86xrsM,1597
108
+ aline_ai-0.6.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
109
+ aline_ai-0.6.7.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
110
+ aline_ai-0.6.7.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
111
+ aline_ai-0.6.7.dist-info/RECORD,,
realign/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.6.6"
6
+ __version__ = "0.6.7"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
realign/agent_names.py CHANGED
@@ -75,5 +75,5 @@ _rng = random.SystemRandom()
75
75
 
76
76
 
77
77
  def generate_agent_name() -> str:
78
- """Return a random Docker-style name, e.g. 'bold-turing'."""
79
- return f"{_rng.choice(ADJECTIVES)}-{_rng.choice(SURNAMES)}"
78
+ """Return a random name, e.g. 'Bold Turing'."""
79
+ return f"{_rng.choice(ADJECTIVES).capitalize()} {_rng.choice(SURNAMES).capitalize()}"
@@ -41,7 +41,18 @@ def _get_db():
41
41
 
42
42
  return get_database(read_only=False)
43
43
  except Exception:
44
- return None
44
+ try:
45
+ import sys
46
+
47
+ root = Path(__file__).resolve().parents[2]
48
+ root_str = str(root)
49
+ if root_str not in sys.path:
50
+ sys.path.insert(0, root_str)
51
+ from realign.db import get_database # type: ignore
52
+
53
+ return get_database(read_only=False)
54
+ except Exception:
55
+ return None
45
56
 
46
57
 
47
58
  def _write_to_db(
@@ -67,6 +78,10 @@ def _write_to_db(
67
78
  if not db:
68
79
  return False
69
80
 
81
+ # Force source to agent mapping when agent_id is known
82
+ if agent_id:
83
+ source = f"agent:{agent_id}"
84
+
70
85
  # Check if agent exists
71
86
  existing = db.get_agent_by_id(terminal_id)
72
87
  if existing:
@@ -105,6 +120,19 @@ def _write_to_db(
105
120
  except Exception:
106
121
  pass
107
122
 
123
+ # WindowLink: record terminal/session association (V23)
124
+ try:
125
+ db.insert_window_link(
126
+ terminal_id=terminal_id,
127
+ agent_id=agent_id,
128
+ session_id=session_id,
129
+ provider=provider,
130
+ source=source,
131
+ ts=time.time(),
132
+ )
133
+ except Exception:
134
+ pass
135
+
108
136
  # Note: Don't close - get_database() returns a singleton
109
137
  return True
110
138
  except Exception:
@@ -134,6 +162,9 @@ def update_terminal_mapping(
134
162
  Concurrency: uses a simple fcntl lock file for JSON; last writer wins, but updates are atomic.
135
163
  """
136
164
  # Phase 1: Write to database (best-effort, don't fail if DB unavailable)
165
+ if agent_id:
166
+ source = f"agent:{agent_id}"
167
+
137
168
  _write_to_db(
138
169
  terminal_id=terminal_id,
139
170
  provider=provider,
realign/codex_detector.py CHANGED
@@ -21,8 +21,23 @@ def _codex_session_roots() -> list[Path]:
21
21
  homes = aline_codex_homes_dir()
22
22
  if homes.exists():
23
23
  for child in homes.iterdir():
24
- if child.is_dir():
25
- roots.append(codex_sessions_dir_for_home(child))
24
+ if not child.is_dir():
25
+ continue
26
+ if child.name.startswith("agent-"):
27
+ # New layout: agent-<id>/<terminal_id>/sessions
28
+ try:
29
+ for grandchild in child.iterdir():
30
+ if grandchild.is_dir():
31
+ nested_sessions = codex_sessions_dir_for_home(grandchild)
32
+ if nested_sessions.exists():
33
+ roots.append(nested_sessions)
34
+ except Exception:
35
+ continue
36
+ else:
37
+ # Terminal layout: <terminal_id>/sessions
38
+ direct_sessions = codex_sessions_dir_for_home(child)
39
+ if direct_sessions.exists():
40
+ roots.append(direct_sessions)
26
41
  except Exception:
27
42
  pass
28
43
 
realign/codex_home.py CHANGED
@@ -39,7 +39,7 @@ def codex_home_for_agent(agent_id: str) -> Path:
39
39
 
40
40
  def codex_home_for_terminal_or_agent(terminal_id: str, agent_id: Optional[str]) -> Path:
41
41
  if agent_id:
42
- return codex_home_for_agent(agent_id)
42
+ return codex_home_for_agent(agent_id) / _safe_id(terminal_id)
43
43
  return codex_home_for_terminal(terminal_id)
44
44
 
45
45
 
@@ -76,11 +76,17 @@ def codex_home_owner_from_session_file(session_file: Path) -> Optional[tuple[str
76
76
  owner = (parts[0] or "").strip()
77
77
  if not owner:
78
78
  return None
79
- if parts[1] != "sessions":
80
- return None
81
- if owner.startswith(AGENT_HOME_PREFIX):
82
- return ("agent", owner[len(AGENT_HOME_PREFIX) :])
83
- return ("terminal", owner)
79
+ # Terminal layout: <homes>/<terminal_id>/sessions/...
80
+ if parts[1] == "sessions":
81
+ if owner.startswith(AGENT_HOME_PREFIX):
82
+ return ("agent", owner[len(AGENT_HOME_PREFIX):])
83
+ return ("terminal", owner)
84
+ # Agent/terminal layout: <homes>/agent-<agent_id>/<terminal_id>/sessions/...
85
+ if owner.startswith(AGENT_HOME_PREFIX) and len(parts) >= 4 and parts[2] == "sessions":
86
+ terminal_id = (parts[1] or "").strip()
87
+ if terminal_id:
88
+ return ("terminal", terminal_id)
89
+ return None
84
90
 
85
91
 
86
92
  def terminal_id_from_codex_session_file(session_file: Path) -> Optional[str]:
@@ -113,4 +119,16 @@ def prepare_codex_home(terminal_id: str, *, agent_id: Optional[str] = None) -> P
113
119
  except Exception:
114
120
  pass
115
121
 
122
+ # Reuse global auth/config to avoid re-login for per-terminal homes.
123
+ try:
124
+ global_home = Path.home() / ".codex"
125
+ for name in ("auth.json", "config.toml"):
126
+ src = global_home / name
127
+ dst = home / name
128
+ if src.exists() and not dst.exists():
129
+ dst.parent.mkdir(parents=True, exist_ok=True)
130
+ dst.symlink_to(src)
131
+ except Exception:
132
+ pass
133
+
116
134
  return home
@@ -435,6 +435,17 @@ def run_doctor(
435
435
  except Exception as e:
436
436
  console.print(f" [yellow]![/yellow] Failed to check jobs: {e}")
437
437
 
438
+ # 5b. Repair agent/session associations
439
+ console.print("\n[bold]5b. Repairing agent/session associations...[/bold]")
440
+ try:
441
+ repaired = _repair_agent_session_links(verbose=verbose)
442
+ if repaired > 0:
443
+ console.print(f" [green]✓[/green] Repaired {repaired} session(s)")
444
+ else:
445
+ console.print(" [green]✓[/green] No missing associations found")
446
+ except Exception as e:
447
+ console.print(f" [yellow]![/yellow] Failed to repair associations: {e}")
448
+
438
449
  # 6. Restart/ensure daemons
439
450
  if restart_daemons:
440
451
  console.print("\n[bold]6. Checking daemons...[/bold]")
@@ -472,6 +483,69 @@ def run_doctor(
472
483
  return 0
473
484
 
474
485
 
486
+ def _repair_agent_session_links(*, verbose: bool = False) -> int:
487
+ """Backfill sessions.agent_id using windowlink and agents mappings."""
488
+ try:
489
+ from ..db import get_database
490
+
491
+ db = get_database(read_only=False)
492
+ except Exception:
493
+ return 0
494
+
495
+ repaired = 0
496
+
497
+ # 1) Use windowlink latest records
498
+ try:
499
+ links = db.list_latest_window_links(limit=5000)
500
+ except Exception:
501
+ links = []
502
+
503
+ for link in links:
504
+ session_id = (link.session_id or "").strip()
505
+ agent_id = (link.agent_id or "").strip()
506
+ if not session_id or not agent_id:
507
+ continue
508
+ try:
509
+ session = db.get_session_by_id(session_id)
510
+ if session and getattr(session, "agent_id", None):
511
+ continue
512
+ except Exception:
513
+ pass
514
+ try:
515
+ db.update_session_agent_id(session_id, agent_id)
516
+ repaired += 1
517
+ except Exception:
518
+ continue
519
+
520
+ # 2) Fallback: agents table mapping (session_id -> agent source)
521
+ try:
522
+ agents = db.list_agents(status=None, limit=5000)
523
+ except Exception:
524
+ agents = []
525
+ for agent in agents:
526
+ session_id = (agent.session_id or "").strip()
527
+ source = (agent.source or "").strip()
528
+ if not session_id or not source.startswith("agent:"):
529
+ continue
530
+ agent_id = source[6:]
531
+ try:
532
+ session = db.get_session_by_id(session_id)
533
+ if session and getattr(session, "agent_id", None):
534
+ continue
535
+ except Exception:
536
+ pass
537
+ try:
538
+ db.update_session_agent_id(session_id, agent_id)
539
+ repaired += 1
540
+ except Exception:
541
+ continue
542
+
543
+ if verbose and repaired:
544
+ console.print(f" [dim]Repaired session links: {repaired}[/dim]")
545
+
546
+ return repaired
547
+
548
+
475
549
  def doctor_command(
476
550
  no_restart: bool = typer.Option(False, "--no-restart", help="Only repair files, don't restart daemons"),
477
551
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
@@ -494,4 +568,3 @@ def doctor_command(
494
568
  clear_cache=True,
495
569
  )
496
570
  raise typer.Exit(code=exit_code)
497
-
@@ -1676,6 +1676,49 @@ def _extend_share_expiry(
1676
1676
  return None
1677
1677
 
1678
1678
 
1679
+ def _update_share_content(
1680
+ backend_url: str,
1681
+ share_id: str,
1682
+ token: str,
1683
+ conversation_data: dict,
1684
+ expected_version: int = 0,
1685
+ ) -> dict:
1686
+ """
1687
+ Push updated content to an existing share via PUT /api/share/{id}.
1688
+
1689
+ Args:
1690
+ backend_url: Backend server URL
1691
+ share_id: Share ID on the server
1692
+ token: Admin or contributor token for auth
1693
+ conversation_data: Full conversation data to replace current content
1694
+ expected_version: Optimistic locking version (409 on mismatch)
1695
+
1696
+ Returns:
1697
+ Response dict with success, version fields
1698
+
1699
+ Raises:
1700
+ RuntimeError on upload failure
1701
+ httpx.HTTPStatusError with 409 status on version conflict
1702
+ """
1703
+ if not HTTPX_AVAILABLE:
1704
+ raise RuntimeError("httpx package not installed. Run: pip install httpx")
1705
+
1706
+ headers = {
1707
+ "X-Token": token,
1708
+ "X-Expected-Version": str(expected_version),
1709
+ "Content-Type": "application/json",
1710
+ }
1711
+
1712
+ response = httpx.put(
1713
+ f"{backend_url}/api/share/{share_id}",
1714
+ headers=headers,
1715
+ json={"conversation_data": conversation_data},
1716
+ timeout=60.0,
1717
+ )
1718
+ response.raise_for_status()
1719
+ return response.json()
1720
+
1721
+
1679
1722
  def _standard_upload(
1680
1723
  encrypted_payload: dict,
1681
1724
  metadata: dict,
@@ -3763,6 +3806,67 @@ def export_agent_shares_command(
3763
3806
  print(f"Error: Agent not found: {agent_id}", file=sys.stderr)
3764
3807
  return 1
3765
3808
 
3809
+ # Check for existing share (re-share → sync instead of new link)
3810
+ if (
3811
+ not password
3812
+ and agent_info.share_url
3813
+ and agent_info.share_id
3814
+ and (agent_info.share_admin_token or agent_info.share_contributor_token)
3815
+ ):
3816
+ _progress("Agent already shared, syncing...")
3817
+ try:
3818
+ from .sync_agent import sync_agent_command
3819
+
3820
+ sync_result = sync_agent_command(
3821
+ agent_id=agent_id,
3822
+ backend_url=backend_url,
3823
+ progress_callback=progress_callback,
3824
+ )
3825
+ if sync_result.get("success"):
3826
+ # Extend expiry if we have admin token
3827
+ if agent_info.share_admin_token:
3828
+ try:
3829
+ _extend_share_expiry(
3830
+ backend_url=backend_url,
3831
+ share_id=agent_info.share_id,
3832
+ admin_token=agent_info.share_admin_token,
3833
+ expiry_days=expiry_days,
3834
+ )
3835
+ except Exception as ext_err:
3836
+ logger.warning(f"Failed to extend share expiry: {ext_err}")
3837
+
3838
+ if json_output:
3839
+ output_data = {
3840
+ "agent_id": agent_id,
3841
+ "agent_name": agent_info.name,
3842
+ "share_link": agent_info.share_url,
3843
+ "synced": True,
3844
+ "sessions_pulled": sync_result.get("sessions_pulled", 0),
3845
+ "sessions_pushed": sync_result.get("sessions_pushed", 0),
3846
+ }
3847
+ print(json.dumps(output_data, ensure_ascii=False, indent=2))
3848
+ else:
3849
+ pulled = sync_result.get("sessions_pulled", 0)
3850
+ pushed = sync_result.get("sessions_pushed", 0)
3851
+ print(f"\n🔄 Synced agent: {agent_info.name}")
3852
+ print(f" Pulled {pulled} session(s), pushed {pushed} session(s)")
3853
+ print(f"🔗 Share link: {agent_info.share_url}")
3854
+ copied = _copy_share_to_clipboard(agent_info.share_url, None)
3855
+ if copied:
3856
+ print("📋 Copied share link to clipboard.")
3857
+ return 0
3858
+ else:
3859
+ err = sync_result.get("error", "Unknown sync error")
3860
+ logger.warning(f"Sync failed, falling through to new share: {err}")
3861
+ if not json_output:
3862
+ print(f"⚠️ Sync failed ({err}), creating new share link...", file=sys.stderr)
3863
+ except ImportError:
3864
+ logger.warning("sync_agent module not available, creating new share")
3865
+ except Exception as e:
3866
+ logger.warning(f"Sync failed, falling through to new share: {e}")
3867
+ if not json_output:
3868
+ print(f"⚠️ Sync failed ({e}), creating new share link...", file=sys.stderr)
3869
+
3766
3870
  # Get sessions for this agent
3767
3871
  session_records = db.get_sessions_by_agent_id(agent_id)
3768
3872
  if not session_records:
@@ -3859,6 +3963,13 @@ def export_agent_shares_command(
3859
3963
  "usage": "Local AI agents can install the aline MCP server and use the 'ask_shared_conversation' tool to query this conversation programmatically.",
3860
3964
  }
3861
3965
 
3966
+ # Include sync_metadata placeholder (contributor_token will be added after upload for unencrypted shares)
3967
+ if not password:
3968
+ conversation_data["sync_metadata"] = {
3969
+ "contributor_token": None, # Will be populated after upload
3970
+ "sync_version": 0,
3971
+ }
3972
+
3862
3973
  # Upload to backend (no encryption for agent shares by default)
3863
3974
  _progress("Uploading to cloud...")
3864
3975
 
@@ -3894,6 +4005,46 @@ def export_agent_shares_command(
3894
4005
  share_url = result.get("share_url")
3895
4006
  slack_message = ui_metadata.get("slack_message") if ui_metadata else None
3896
4007
 
4008
+ # Store sync metadata for unencrypted shares
4009
+ if not password and share_url:
4010
+ share_id_result = result.get("share_id") or _extract_share_id_from_url(share_url)
4011
+ admin_token = result.get("admin_token")
4012
+ contributor_token = result.get("contributor_token")
4013
+ expiry_at = result.get("expiry_at")
4014
+
4015
+ if share_id_result:
4016
+ try:
4017
+ db.update_agent_sync_metadata(
4018
+ agent_id,
4019
+ share_id=share_id_result,
4020
+ share_url=share_url,
4021
+ share_admin_token=admin_token,
4022
+ share_contributor_token=contributor_token,
4023
+ share_expiry_at=expiry_at,
4024
+ last_synced_at=datetime.now(timezone.utc).isoformat(),
4025
+ sync_version=0,
4026
+ )
4027
+ except Exception as e:
4028
+ logger.warning(f"Failed to store sync metadata: {e}")
4029
+
4030
+ # Re-upload with contributor_token embedded in sync_metadata
4031
+ # so importers can get it
4032
+ if contributor_token:
4033
+ try:
4034
+ conversation_data["sync_metadata"] = {
4035
+ "contributor_token": contributor_token,
4036
+ "sync_version": 0,
4037
+ }
4038
+ _update_share_content(
4039
+ backend_url=backend_url,
4040
+ share_id=share_id_result,
4041
+ token=admin_token or contributor_token,
4042
+ conversation_data=conversation_data,
4043
+ expected_version=0,
4044
+ )
4045
+ except Exception as e:
4046
+ logger.warning(f"Failed to re-upload with sync metadata: {e}")
4047
+
3897
4048
  # Output results
3898
4049
  if json_output:
3899
4050
  output_data = {