aline-ai 0.7.3__py3-none-any.whl → 0.7.5__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.7.3
3
+ Version: 0.7.5
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,70 +1,75 @@
1
- aline_ai-0.7.3.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=ReJTANkYJVgZYwHExBSBOBiJZxdi2HY4VluOTHSxRns,1623
1
+ aline_ai-0.7.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=IirhpRnTbBI_00iPya-W-qJQt2x8bWszi8GFYBZH8GM,1623
3
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=PiMUA_sFQ-K7zlIr1Ahs7St8NwcXDG3JKT_8yIqLwZI,40569
7
7
  realign/codex_detector.py,sha256=WGIClvlrFVCqJ5vR9DrKVsp1eJhOShvcaXibTHb0Nfc,6304
8
- realign/codex_home.py,sha256=ljkW8uCfQD4cisEJtPNQmIgaR0yEfWSyHwoVQFY-6p4,4374
8
+ realign/codex_home.py,sha256=lYUmzhKRXDm6g1foGtd7P8mXwJ5-ssRBj1s8PI-pq98,6430
9
9
  realign/codex_terminal_linker.py,sha256=L2Ha4drlZ7Sbq2jzXyxczOdUY3S5fu1gJqoI5WN9CKk,6211
10
- realign/config.py,sha256=_loJkoTKszMONgo6Qq3N8VRm_iqvD-7WvXeCsKUgGUE,9478
10
+ realign/commit_pipeline.py,sha256=V_IfSNmnNE8sah2fo8d7sodJG3F-7hNo51f6hLiTbFQ,43675
11
+ realign/config.py,sha256=DHchxsjL86VTAtjKzgnnb4lhH5EGndX5dzhE1IVg3FQ,8990
11
12
  realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
12
13
  realign/file_lock.py,sha256=kLNm1Rra4TCrTMyPM5fwjVascq-CUz2Bzh9HHKtCKOE,3444
13
14
  realign/hooks.py,sha256=wSSIjS5x9w7fm9LUcL63Lf7bglEfb75dHFja_znKDDQ,65134
14
15
  realign/llm_client.py,sha256=QqMPDFE-aXm7oz0QAkB90CN0Qn0uz7JOxpWbUUlHNgU,11141
15
- realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,4871
16
+ realign/logging_config.py,sha256=egZr94-EjDUK2cpHU8ePm3v7TuxBlUdUi7f7YQM-cz0,6488
16
17
  realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
17
18
  realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
18
19
  realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
19
- realign/watcher_core.py,sha256=iXVXlDhYsoxk_BZ1rOVIfTDu6ABXZO18UeNQKVPsvzc,120374
20
+ realign/watcher_core.py,sha256=SmntxPmS2ECixoVk0wfj_4w8DpkxQXu6bvy3npgIHVE,131642
20
21
  realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
21
- realign/worker_core.py,sha256=tJGETH_FIBpb899D8-JP_cOyDE63BYc8kxxmrhb83i4,13013
22
+ realign/worker_core.py,sha256=kNxNFj1Z4bZqBYhOMBzeBxX8mmFwg7hTXSsydK3KugE,21281
22
23
  realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
23
24
  realign/adapters/__init__.py,sha256=alkJr7DRn_CrJecSJRjRJOHHnkz9EnZ5TnsU8n1Bb0k,719
24
25
  realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
25
26
  realign/adapters/claude.py,sha256=ksTRwC5Z8AzUcB21LFjx6DETP08cv__fjgBzm-TeZdI,5444
26
- realign/adapters/codex.py,sha256=VJgmrRzOO5a6GNG6xL7gJwzcvA2CBmHdilYkj0qffBw,2233
27
+ realign/adapters/codex.py,sha256=Ull4rzWSGkVOBPwM0pI_dXUTNexQ6pi5IwzIbVcKLas,3361
27
28
  realign/adapters/gemini.py,sha256=NvtXQPWUtEY-DaAAMvLGvQW4FalTG-g0pD514HYnzF0,2540
28
29
  realign/adapters/registry.py,sha256=yM6nf9nGTJ1vaK2Uixp-VacseK7PmxZkCdKedmWI8MA,3255
29
30
  realign/claude_hooks/__init__.py,sha256=MT9c8TWjLO23xDCM-uBBMy_mOThNd7O-AgN_Khn30qs,594
30
31
  realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFrooDEcd9KxtmF2-3nCw,6448
31
32
  realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
32
- realign/claude_hooks/stop_hook.py,sha256=Bzf6CjHQ-0q61SrDrpIvcwt_BmDO1FE-f8cws_aA-Is,13582
33
+ realign/claude_hooks/stop_hook.py,sha256=9LfQqKAoi09t2XzVbFEPWtGlduK7Id82Ue8Qs5G21Gk,19532
33
34
  realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
34
35
  realign/claude_hooks/terminal_state.py,sha256=2ygTbVnh2b59vRLuN-TyWcXR94NKFlaVwOhS3ipqn58,6647
35
36
  realign/claude_hooks/user_prompt_submit_hook.py,sha256=8e0zNonT95TH2uuISYp3am_RD7c84Ghh1WRPgs023DI,10625
36
37
  realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
38
+ realign/codex_hooks/__init__.py,sha256=lmmfnCdVyTKxPytahAkcp81TT7kZXsK-1jQzNUs_03A,488
39
+ realign/codex_hooks/notify_hook.py,sha256=HD4f1VTRSJYzjxLQ-FGyKdX9biqvf4rEaqU-PtmX8F8,17058
40
+ realign/codex_hooks/notify_hook_installer.py,sha256=zDE4qDelH4dBHBgK_c-ExvYGfCUWqr_l8zQhxSRE9KU,8054
37
41
  realign/commands/__init__.py,sha256=WVaVT1orM2Z0PYaG3X6tkKb_t2v3n_3siCadh1qd_QA,107
38
42
  realign/commands/add.py,sha256=_Xzt9P15mwndA3JvBBVrki8tn9Cc0UP6SiLwM4RS8Nc,27232
39
43
  realign/commands/agent.py,sha256=3CS48bMn7tkdDWKRrfg7CYbhcJK4Pz40YjYMvwD7c2w,3173
40
44
  realign/commands/auth.py,sha256=wcs1lUcSXxv75WcGruzyZ3kgi0xXA8W4lNnUwM4a3CI,11731
41
45
  realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
42
46
  realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
43
- realign/commands/doctor.py,sha256=0c1TZuA_cw1CSU0yKMVRU-18uTxdqjXKJ8lP2CTTNSQ,20656
47
+ realign/commands/doctor.py,sha256=P_XL8UmVzXPsLpCLUK1Uv50GxjkQ5kiBLHO3a0ljTRU,25648
44
48
  realign/commands/export_shares.py,sha256=O2yRZT4S2ANoswLwDDmA1mau1nEvBVbmSXD4ST6Id_o,153150
45
- realign/commands/import_shares.py,sha256=Jx_7HVSg7SrGGKLDxsf_UqoStDimw8B26uKkqNFF6t8,33071
46
- realign/commands/init.py,sha256=6rBr1LVIrQLbUH_UvoDhkF1qXmMh2xkjNWCYAUz5Tho,35274
49
+ realign/commands/import_shares.py,sha256=FvNcpPvxEa2uoaB9ZDJTogWAFZc27WxHJ3u_26xvGh8,33592
50
+ realign/commands/init.py,sha256=y9GUJcOyLm9ZBl5XPdqaG6-HmN0q3oWXzMKohXzMcwA,36024
47
51
  realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
48
52
  realign/commands/search.py,sha256=QlUDzRDD6ebq21LTtLe5-OZM62iwDrDqfbnXbuxfklU,27516
49
- realign/commands/sync_agent.py,sha256=sopzUQ6kiRgiBlcEReGAWCRoqrHpk3nAx75qXSgnNi4,17082
50
- realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
53
+ realign/commands/sync_agent.py,sha256=-ze0bXwABmO77d1EenObBPg0coAHgAX1rVEeJGXJjrg,24799
54
+ realign/commands/upgrade.py,sha256=e_U15s-3S2W2lCqGXuvcBhYCPxFJ4cGzGF7MNCWdG30,17050
51
55
  realign/commands/watcher.py,sha256=4WTThIgr-Z5guKh_JqGDcPmerr97XiHrVaaijmckHsA,134350
52
56
  realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
53
57
  realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
54
- realign/dashboard/app.py,sha256=XLPqvPwGuR5Tyu6uz9T88yQSc4wq8Afu0h7pWH5A8_k,8161
58
+ realign/dashboard/app.py,sha256=Ot1wHjBZlO35npTc9osNNPxNH3flBxBod-nb71C956Y,14027
55
59
  realign/dashboard/clipboard.py,sha256=81frq83E_urqLkwuCvtl0hiTEjavtdQn8kCi72jJWcs,1207
60
+ realign/dashboard/diagnostics.py,sha256=f3BaxgDHszmMrgoFRtyhzjhDi5b_6p42TyTELCC9fB8,8522
56
61
  realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
57
62
  realign/dashboard/local_api.py,sha256=Roq74etTJR0uOiHE3uIe7sqVITjS5JGQEF4g0nmUm5Q,4332
58
63
  realign/dashboard/state.py,sha256=V7zBKvyDgqdXv68XHxV4T8xf3IhYbI5W33UmYW3_hyM,1139
59
64
  realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
60
- realign/dashboard/tmux_manager.py,sha256=HJwB2Wpz-I4OrNT3Db8gKCLifmHdMCalA-UONBaLMG8,34564
65
+ realign/dashboard/tmux_manager.py,sha256=aDL2wpRvEKnTG1QBkh4i__0_6xRa-n9412cfQcMzM18,46440
61
66
  realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
62
67
  realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
63
68
  realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
64
69
  realign/dashboard/screens/__init__.py,sha256=MiefFamCYRrzTwQXiCUdybaJaFxlK5XKtLHaSQmqDv0,597
65
70
  realign/dashboard/screens/agent_detail.py,sha256=N-iUC4434C91OcDu4dkQaxS_NXQ5Yl5sqNBb2mTmoBw,10490
66
- realign/dashboard/screens/create_agent.py,sha256=Dy9liP_4fj_zgNafRRJGX2iQJiarHvtVLdghrqMGiLQ,11323
67
- realign/dashboard/screens/create_agent_info.py,sha256=K2Rbp4zHVdanPT3Fp82We4qlSAM-0IBZXPLuQuevuME,7838
71
+ realign/dashboard/screens/create_agent.py,sha256=-r95kIOfbwRuUnuFuJTEl7wXxsZh03hKswj8r8odACE,11348
72
+ realign/dashboard/screens/create_agent_info.py,sha256=42mjyFl56XjDM8qLjX2Wu6v3h1rNy7V_084M1UR8X7o,6602
68
73
  realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
69
74
  realign/dashboard/screens/event_detail.py,sha256=-pqt3NBoeTXGJKtbndZy-msklwXTeNWMS4H12oMG5ks,20175
70
75
  realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
@@ -72,10 +77,10 @@ realign/dashboard/screens/session_detail.py,sha256=TBkHqSHyMxsLB2QdZq9m1EoiH8oRV
72
77
  realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
73
78
  realign/dashboard/styles/dashboard.tcss,sha256=9W5Tx0lgyGb4HU-z-Kn7gBdexIK0aPe0bkVn2k_AseM,3288
74
79
  realign/dashboard/widgets/__init__.py,sha256=dXsOnbeu_8XhP-6Bu6-R_0LNGqsSM6x7dG7FCDumpa8,460
75
- realign/dashboard/widgets/agents_panel.py,sha256=pqXZhzSL84lzJPqGGGsfsGJGVlVo2iCyHByXM4_ITCM,47083
76
- realign/dashboard/widgets/config_panel.py,sha256=J6A_rxGVqNu5TMFcWELWgdX1nFCHAjKprFMMp7mBDKo,18203
80
+ realign/dashboard/widgets/agents_panel.py,sha256=XuywG4LR4LrS4WSdVHAREqtHFjV54C045jZvtWiN8rs,71027
81
+ realign/dashboard/widgets/config_panel.py,sha256=yZPymOZ5gZ4RCJE9iZ3AYUHpH9CSpSBIa3tQslUi39Q,16006
77
82
  realign/dashboard/widgets/events_table.py,sha256=0cMvE0KdZFBZyvywv7vlt005qsR0aLQnQiMf3ZzK7RY,30218
78
- realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
83
+ realign/dashboard/widgets/header.py,sha256=o6lQwNXvaxKwJQz0lep20ymoaT34gSb71zT7IkZe5D0,1562
79
84
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
80
85
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
81
86
  realign/dashboard/widgets/sessions_table.py,sha256=6y78pEkyAmNsU4_o46PbwXRFW17fc5khgheBi4LjBNg,33374
@@ -87,7 +92,7 @@ realign/db/locks.py,sha256=dUQu9Yo5nZstMSPXZPYzN0xqX8UXhJgNV_PmYEJ-rK0,1801
87
92
  realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
88
93
  realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
89
94
  realign/db/schema.py,sha256=IWPbeDYrbC1eZGQAy8k1rk0r2NnABJzXSSg8bb00XBw,33885
90
- realign/db/sqlite_db.py,sha256=u4yybbXzOApYPnHkHlR59qBSyWPoIqgRppTB4ht5taM,119736
95
+ realign/db/sqlite_db.py,sha256=PjI3dinmx7eXGp_JmmZh7qpNWiA85ac65DlkcTSXAf0,121955
91
96
  realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
92
97
  realign/events/agent_summarizer.py,sha256=vh65tYgo1NOYsIpVPR253nnOr-MIejC4KG5dGvDzKv4,5413
93
98
  realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
@@ -106,8 +111,8 @@ realign/triggers/next_turn_trigger.py,sha256=-x80_I-WmIjXXzQHEPBykgx_GQW6oKaLDQx
106
111
  realign/triggers/registry.py,sha256=dkIjSd8Bg-hF0nxaO2Fi2K-0Zipqv6vVjc-HYSrA_fY,3656
107
112
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
108
113
  realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
109
- aline_ai-0.7.3.dist-info/METADATA,sha256=qbtgEyiKE5FSJk_zjGsOTKm8s89Ckqpnw8wGM8RFezA,1597
110
- aline_ai-0.7.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
111
- aline_ai-0.7.3.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
112
- aline_ai-0.7.3.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
113
- aline_ai-0.7.3.dist-info/RECORD,,
114
+ aline_ai-0.7.5.dist-info/METADATA,sha256=-WmXf4ezxtC-BD-zb-NoywchU0NF89UzsoxAwNOZ6-g,1597
115
+ aline_ai-0.7.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
116
+ aline_ai-0.7.5.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
117
+ aline_ai-0.7.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
118
+ aline_ai-0.7.5.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.7.3"
6
+ __version__ = "0.7.5"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
realign/adapters/codex.py CHANGED
@@ -4,6 +4,7 @@ Codex Adapter
4
4
  Handles session discovery and interaction for Codex CLI.
5
5
  """
6
6
 
7
+ import os
7
8
  import json
8
9
  from pathlib import Path
9
10
  from typing import List, Optional, Dict, Any
@@ -19,8 +20,20 @@ class CodexAdapter(SessionAdapter):
19
20
  name = "codex"
20
21
  trigger_class = CodexTrigger
21
22
 
23
+ def _discovery_days_back(self) -> int:
24
+ """
25
+ Limit Codex discovery to recent days to avoid scanning the entire history.
26
+
27
+ Set via ALINE_CODEX_DISCOVERY_DAYS (default: 2).
28
+ """
29
+ raw = os.environ.get("ALINE_CODEX_DISCOVERY_DAYS", "2")
30
+ try:
31
+ return max(0, int(raw))
32
+ except Exception:
33
+ return 2
34
+
22
35
  def discover_sessions(self) -> List[Path]:
23
- """Find all Codex sessions."""
36
+ """Find active/recent Codex sessions (bounded scan)."""
24
37
  sessions: list[Path] = []
25
38
  roots: list[Path] = []
26
39
  try:
@@ -30,11 +43,26 @@ class CodexAdapter(SessionAdapter):
30
43
  except Exception:
31
44
  roots = [Path.home() / ".codex" / "sessions"]
32
45
 
46
+ days_back = self._discovery_days_back()
47
+
33
48
  for root in roots:
34
49
  if not root.exists():
35
50
  continue
36
51
  try:
37
- sessions.extend(root.rglob("rollout-*.jsonl"))
52
+ # Prefer YYYY/MM/DD layout; only scan recent dates.
53
+ if days_back > 0:
54
+ from datetime import datetime, timedelta
55
+
56
+ now = datetime.now()
57
+ for days_ago in range(days_back + 1):
58
+ dt = now - timedelta(days=days_ago)
59
+ date_path = root / str(dt.year) / f"{dt.month:02d}" / f"{dt.day:02d}"
60
+ if not date_path.exists():
61
+ continue
62
+ sessions.extend(date_path.glob("rollout-*.jsonl"))
63
+ else:
64
+ # If explicitly disabled, only look at top-level.
65
+ sessions.extend(root.glob("rollout-*.jsonl"))
38
66
  except Exception:
39
67
  continue
40
68
 
@@ -26,6 +26,8 @@ import sys
26
26
  import json
27
27
  import time
28
28
  import subprocess
29
+ import sqlite3
30
+ import uuid
29
31
  from pathlib import Path
30
32
 
31
33
  try:
@@ -41,6 +43,142 @@ def get_signal_dir() -> Path:
41
43
  return signal_dir
42
44
 
43
45
 
46
+ def _parse_config_sqlite_db_path(config_path: Path) -> str | None:
47
+ """Best-effort parse `sqlite_db_path` from ~/.aline/config.yaml without PyYAML."""
48
+ try:
49
+ for raw_line in config_path.read_text(encoding="utf-8").splitlines():
50
+ line = raw_line.strip()
51
+ if not line or line.startswith("#"):
52
+ continue
53
+ if not line.startswith("sqlite_db_path:"):
54
+ continue
55
+ _, value = line.split(":", 1)
56
+ value = value.strip()
57
+ if (value.startswith('"') and value.endswith('"')) or (
58
+ value.startswith("'") and value.endswith("'")
59
+ ):
60
+ value = value[1:-1]
61
+ return value.strip() or None
62
+ except Exception:
63
+ return None
64
+ return None
65
+
66
+
67
+ def _resolve_sqlite_db_path() -> Path:
68
+ """Resolve Aline sqlite DB path.
69
+
70
+ Keep this lightweight because this script is executed as a Claude hook.
71
+ """
72
+ env_db_path = (
73
+ os.getenv("REALIGN_SQLITE_DB_PATH")
74
+ or os.getenv("REALIGN_DB_PATH")
75
+ or os.getenv("ALINE_DB_PATH")
76
+ )
77
+ if env_db_path:
78
+ return Path(env_db_path).expanduser()
79
+
80
+ config_path = Path.home() / ".aline" / "config.yaml"
81
+ cfg = _parse_config_sqlite_db_path(config_path) if config_path.exists() else None
82
+ if cfg:
83
+ return Path(cfg).expanduser()
84
+
85
+ return Path.home() / ".aline" / "db" / "aline.db"
86
+
87
+
88
+ def _try_enqueue_session_process_job(
89
+ *,
90
+ session_id: str,
91
+ session_file_path: str,
92
+ workspace_path: str | None,
93
+ session_type: str | None,
94
+ source_event: str | None,
95
+ no_track: bool,
96
+ agent_id: str | None,
97
+ connect_timeout_seconds: float,
98
+ ) -> bool:
99
+ """Best-effort enqueue into sqlite jobs table. Never raises."""
100
+ try:
101
+ db_path = _resolve_sqlite_db_path()
102
+ if not db_path.exists():
103
+ return False
104
+
105
+ payload: dict = {"session_id": session_id, "session_file_path": session_file_path}
106
+ if workspace_path is not None:
107
+ payload["workspace_path"] = workspace_path
108
+ if session_type:
109
+ payload["session_type"] = session_type
110
+ if source_event:
111
+ payload["source_event"] = source_event
112
+ if no_track:
113
+ payload["no_track"] = True
114
+ if agent_id:
115
+ payload["agent_id"] = agent_id
116
+
117
+ job_id = str(uuid.uuid4())
118
+ payload_json = json.dumps(payload, ensure_ascii=False)
119
+ dedupe_key = f"session_process:{session_id}"
120
+
121
+ conn = sqlite3.connect(str(db_path), timeout=float(connect_timeout_seconds))
122
+ try:
123
+ conn.execute(
124
+ """
125
+ INSERT INTO jobs (
126
+ id, kind, dedupe_key, payload, status, priority, attempts, next_run_at,
127
+ locked_until, locked_by, reschedule, last_error, created_at, updated_at
128
+ ) VALUES (
129
+ ?, ?, ?, ?, 'queued', ?, 0, datetime('now'),
130
+ NULL, NULL, 0, NULL, datetime('now'), datetime('now')
131
+ )
132
+ ON CONFLICT(dedupe_key) DO UPDATE SET
133
+ kind=excluded.kind,
134
+ payload=excluded.payload,
135
+ priority=MAX(COALESCE(jobs.priority, 0), COALESCE(excluded.priority, 0)),
136
+ attempts=CASE
137
+ WHEN jobs.status='retry' THEN 0
138
+ ELSE COALESCE(jobs.attempts, 0)
139
+ END,
140
+ updated_at=datetime('now'),
141
+ reschedule=CASE
142
+ WHEN jobs.status='processing' THEN 1
143
+ ELSE COALESCE(jobs.reschedule, 0)
144
+ END,
145
+ last_error=CASE
146
+ WHEN jobs.status='retry' THEN NULL
147
+ ELSE jobs.last_error
148
+ END,
149
+ status=CASE
150
+ WHEN jobs.status='processing' THEN jobs.status
151
+ WHEN jobs.status='queued' THEN jobs.status
152
+ WHEN jobs.status='retry' THEN 'queued'
153
+ WHEN jobs.status='done' THEN 'queued'
154
+ ELSE 'queued'
155
+ END,
156
+ next_run_at=CASE
157
+ WHEN jobs.status='processing' THEN jobs.next_run_at
158
+ WHEN jobs.next_run_at IS NULL THEN excluded.next_run_at
159
+ WHEN excluded.next_run_at < jobs.next_run_at THEN excluded.next_run_at
160
+ ELSE jobs.next_run_at
161
+ END
162
+ """,
163
+ (
164
+ job_id,
165
+ "session_process",
166
+ dedupe_key,
167
+ payload_json,
168
+ 15,
169
+ ),
170
+ )
171
+ conn.commit()
172
+ return True
173
+ finally:
174
+ try:
175
+ conn.close()
176
+ except Exception:
177
+ pass
178
+ except Exception:
179
+ return False
180
+
181
+
44
182
  def main():
45
183
  """主函数"""
46
184
  try:
@@ -93,31 +231,48 @@ def main():
93
231
  if not session_id:
94
232
  session_id = f"unknown_{int(time.time() * 1000)}"
95
233
 
96
- # 写入信号文件
97
- signal_dir = get_signal_dir()
98
- timestamp_ms = int(time.time() * 1000)
99
- signal_file = signal_dir / f"{session_id}_{timestamp_ms}.signal"
100
- tmp_file = signal_dir / f"{session_id}_{timestamp_ms}.signal.tmp"
101
-
102
234
  # Check for no-track mode
103
235
  no_track = os.environ.get("ALINE_NO_TRACK", "") == "1"
104
236
 
105
- signal_data = {
106
- "session_id": session_id,
107
- "terminal_id": terminal_id,
108
- "agent_id": agent_id,
109
- "project_dir": project_dir,
110
- "transcript_path": transcript_path,
111
- "cwd": cwd,
112
- "timestamp": time.time(),
113
- "hook_event": "Stop",
114
- }
115
- if no_track:
116
- signal_data["no_track"] = True
237
+ # Fast path: enqueue directly (no signal polling) if transcript_path is available.
238
+ disable_direct_enqueue = os.environ.get("ALINE_STOP_HOOK_DISABLE_DB_ENQUEUE", "") == "1"
239
+ connect_timeout = float(os.environ.get("ALINE_STOP_HOOK_DB_TIMEOUT", "0.2"))
240
+
241
+ enqueued = False
242
+ if (not disable_direct_enqueue) and transcript_path:
243
+ enqueued = _try_enqueue_session_process_job(
244
+ session_id=session_id,
245
+ session_file_path=transcript_path,
246
+ workspace_path=project_dir or None,
247
+ session_type="claude",
248
+ source_event="stop",
249
+ no_track=no_track,
250
+ agent_id=agent_id or None,
251
+ connect_timeout_seconds=connect_timeout,
252
+ )
253
+
254
+ if not enqueued:
255
+ # Fallback: write a stop signal file so the watcher can pick it up.
256
+ signal_dir = get_signal_dir()
257
+ timestamp_ms = int(time.time() * 1000)
258
+ signal_file = signal_dir / f"{session_id}_{timestamp_ms}.signal"
259
+ tmp_file = signal_dir / f"{session_id}_{timestamp_ms}.signal.tmp"
260
+ signal_data = {
261
+ "session_id": session_id,
262
+ "terminal_id": terminal_id,
263
+ "agent_id": agent_id,
264
+ "project_dir": project_dir,
265
+ "transcript_path": transcript_path,
266
+ "cwd": cwd,
267
+ "timestamp": time.time(),
268
+ "hook_event": "Stop",
269
+ }
270
+ if no_track:
271
+ signal_data["no_track"] = True
117
272
 
118
- # Write atomically to avoid watcher reading a partial JSON file.
119
- tmp_file.write_text(json.dumps(signal_data, indent=2))
120
- tmp_file.replace(signal_file)
273
+ # Write atomically to avoid watcher reading a partial JSON file.
274
+ tmp_file.write_text(json.dumps(signal_data, indent=2))
275
+ tmp_file.replace(signal_file)
121
276
 
122
277
  # Best-effort: tag the tmux "terminal tab" with the Claude session id.
123
278
  try:
realign/codex_home.py CHANGED
@@ -89,6 +89,44 @@ def codex_home_owner_from_session_file(session_file: Path) -> Optional[tuple[str
89
89
  return None
90
90
 
91
91
 
92
+ def codex_home_from_session_file(session_file: Path) -> Optional[Path]:
93
+ """Best-effort: infer CODEX_HOME for a Codex session file path."""
94
+ try:
95
+ homes = aline_codex_homes_dir().resolve()
96
+ p = session_file.resolve()
97
+ except Exception:
98
+ homes = None
99
+ p = session_file
100
+
101
+ if homes is not None:
102
+ try:
103
+ rel = p.relative_to(homes)
104
+ except ValueError:
105
+ rel = None
106
+ except Exception:
107
+ rel = None
108
+
109
+ if rel is not None:
110
+ parts = rel.parts
111
+ if len(parts) >= 2 and parts[1] == "sessions":
112
+ return (homes / parts[0]).resolve()
113
+ if (
114
+ len(parts) >= 3
115
+ and (parts[0] or "").startswith(AGENT_HOME_PREFIX)
116
+ and parts[2] == "sessions"
117
+ ):
118
+ return (homes / parts[0] / parts[1]).resolve()
119
+
120
+ try:
121
+ for parent in p.parents:
122
+ if parent.name == "sessions":
123
+ return parent.parent
124
+ except Exception:
125
+ return None
126
+
127
+ return None
128
+
129
+
92
130
  def terminal_id_from_codex_session_file(session_file: Path) -> Optional[str]:
93
131
  """If session_file is under an Aline-managed CODEX_HOME, return terminal_id."""
94
132
  owner = codex_home_owner_from_session_file(session_file)
@@ -99,6 +137,30 @@ def terminal_id_from_codex_session_file(session_file: Path) -> Optional[str]:
99
137
  return owner[1]
100
138
 
101
139
 
140
+ def agent_id_from_codex_session_file(session_file: Path) -> Optional[str]:
141
+ """If session_file is under an Aline-managed agent CODEX_HOME, return agent_id."""
142
+ try:
143
+ homes = aline_codex_homes_dir().resolve()
144
+ p = session_file.resolve()
145
+ except Exception:
146
+ return None
147
+
148
+ try:
149
+ rel = p.relative_to(homes)
150
+ except ValueError:
151
+ return None
152
+
153
+ parts = rel.parts
154
+ if not parts:
155
+ return None
156
+
157
+ owner = (parts[0] or "").strip()
158
+ if not owner.startswith(AGENT_HOME_PREFIX):
159
+ return None
160
+ agent_id = owner[len(AGENT_HOME_PREFIX) :].strip()
161
+ return agent_id or None
162
+
163
+
102
164
  def prepare_codex_home(terminal_id: str, *, agent_id: Optional[str] = None) -> Path:
103
165
  """Create/prepare an isolated CODEX_HOME (per-agent if agent_id is provided)."""
104
166
  home = codex_home_for_terminal_or_agent(terminal_id, agent_id)
@@ -131,4 +193,13 @@ def prepare_codex_home(terminal_id: str, *, agent_id: Optional[str] = None) -> P
131
193
  except Exception:
132
194
  pass
133
195
 
196
+ # Best-effort: ensure notify hook is installed inside this CODEX_HOME so watcher
197
+ # does not need polling for Codex turns.
198
+ try:
199
+ from .codex_hooks.notify_hook_installer import ensure_notify_hook_installed_for_codex_home
200
+
201
+ ensure_notify_hook_installed_for_codex_home(home, quiet=True)
202
+ except Exception:
203
+ pass
204
+
134
205
  return home
@@ -0,0 +1,16 @@
1
+ """Codex CLI hook integrations.
2
+
3
+ This package contains best-effort hooks/installers for integrating with the
4
+ OpenAI Codex CLI. Unlike Claude Code, Codex hooks are configured via Codex
5
+ configuration files (e.g. config.toml notify hook).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+
13
+ def codex_notify_signal_dir() -> Path:
14
+ """Directory for fallback notify signals (when direct DB enqueue fails)."""
15
+ return Path.home() / ".aline" / ".signals" / "codex_notify"
16
+