aline-ai 0.7.2__py3-none-any.whl → 0.7.4__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.
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/METADATA +1 -1
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/RECORD +32 -27
- realign/__init__.py +1 -1
- realign/adapters/codex.py +30 -2
- realign/claude_hooks/stop_hook.py +176 -21
- realign/codex_home.py +71 -0
- realign/codex_hooks/__init__.py +16 -0
- realign/codex_hooks/notify_hook.py +511 -0
- realign/codex_hooks/notify_hook_installer.py +247 -0
- realign/commands/doctor.py +125 -0
- realign/commands/export_shares.py +188 -65
- realign/commands/import_shares.py +30 -10
- realign/commands/init.py +16 -0
- realign/commands/sync_agent.py +274 -44
- realign/commit_pipeline.py +1024 -0
- realign/config.py +3 -11
- realign/dashboard/app.py +151 -2
- realign/dashboard/diagnostics.py +274 -0
- realign/dashboard/screens/create_agent.py +2 -1
- realign/dashboard/screens/create_agent_info.py +40 -77
- realign/dashboard/tmux_manager.py +348 -33
- realign/dashboard/widgets/agents_panel.py +942 -314
- realign/dashboard/widgets/config_panel.py +34 -121
- realign/dashboard/widgets/header.py +1 -1
- realign/db/sqlite_db.py +59 -1
- realign/logging_config.py +51 -6
- realign/watcher_core.py +742 -393
- realign/worker_core.py +206 -15
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/top_level.txt +0 -0
|
@@ -1,70 +1,75 @@
|
|
|
1
|
-
aline_ai-0.7.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.7.4.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=4YDOjZrTvdzz5ddO6-cwuKux4QKiTgmSFG5zrMzRo58,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=
|
|
8
|
+
realign/codex_home.py,sha256=lYUmzhKRXDm6g1foGtd7P8mXwJ5-ssRBj1s8PI-pq98,6430
|
|
9
9
|
realign/codex_terminal_linker.py,sha256=L2Ha4drlZ7Sbq2jzXyxczOdUY3S5fu1gJqoI5WN9CKk,6211
|
|
10
|
-
realign/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
44
|
-
realign/commands/export_shares.py,sha256=
|
|
45
|
-
realign/commands/import_shares.py,sha256=
|
|
46
|
-
realign/commands/init.py,sha256=
|
|
47
|
+
realign/commands/doctor.py,sha256=P_XL8UmVzXPsLpCLUK1Uv50GxjkQ5kiBLHO3a0ljTRU,25648
|
|
48
|
+
realign/commands/export_shares.py,sha256=O2yRZT4S2ANoswLwDDmA1mau1nEvBVbmSXD4ST6Id_o,153150
|
|
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
|
|
53
|
+
realign/commands/sync_agent.py,sha256=-ze0bXwABmO77d1EenObBPg0coAHgAX1rVEeJGXJjrg,24799
|
|
50
54
|
realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
|
|
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=
|
|
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=
|
|
65
|
+
realign/dashboard/tmux_manager.py,sha256=jTEU_0lqPfSJ7Q1fG77miY9fEctbQB5-qaDOW-prDKI,45767
|
|
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
|
|
67
|
-
realign/dashboard/screens/create_agent_info.py,sha256=
|
|
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=
|
|
76
|
-
realign/dashboard/widgets/config_panel.py,sha256=
|
|
80
|
+
realign/dashboard/widgets/agents_panel.py,sha256=Qi76wfiSSK_Qn3TpQEVW8g2cltPQcG0RuzWi8F0HCXo,70815
|
|
81
|
+
realign/dashboard/widgets/config_panel.py,sha256=lQqAO2OEUzZIMn9qGiRJsv32D1sRb3XnjjiyYvXyS5E,15324
|
|
77
82
|
realign/dashboard/widgets/events_table.py,sha256=0cMvE0KdZFBZyvywv7vlt005qsR0aLQnQiMf3ZzK7RY,30218
|
|
78
|
-
realign/dashboard/widgets/header.py,sha256=
|
|
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=
|
|
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.
|
|
110
|
-
aline_ai-0.7.
|
|
111
|
-
aline_ai-0.7.
|
|
112
|
-
aline_ai-0.7.
|
|
113
|
-
aline_ai-0.7.
|
|
114
|
+
aline_ai-0.7.4.dist-info/METADATA,sha256=roEHsnZg0iMKmmkXAL0hykWcZtI3wV-Qk2vtBIf2feo,1597
|
|
115
|
+
aline_ai-0.7.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
116
|
+
aline_ai-0.7.4.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
117
|
+
aline_ai-0.7.4.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
118
|
+
aline_ai-0.7.4.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
|