Repository navigation
Expand file tree
/
Copy pathpost-init.el
More file actions
314 lines (282 loc) · 15.9 KB
/
Copy pathpost-init.el
File metadata and controls
314 lines (282 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
;;; post-init.el --- User configuration -*- no-byte-compile: t; lexical-binding: t; -*-
;;; Commentary:
;; Personal configuration and overrides. Add your customizations here.
;; This file is loaded after all modular configuration files.
;;; Code:
;; Pre-emptively set persistent-headless for workflow daemons before any
;; gptel-mode buffers are created. Without this the gptel-mode hook in
;; gptel-ext-core.el defaults every buffer's gptel-backend to MiniMax,
;; and the headless-provider-override never activates because it requires
;; this variable to be non-nil.
(when (string= (getenv "MINIMAL_EMACS_WORKFLOW_DAEMON") "1")
(setq gptel-auto-workflow-persistent-headless t)
;; Clean stale daemon socket before server-start creates a new one.
(when (boundp 'server-socket-dir)
(condition-case nil
(let* ((socket-dir server-socket-dir)
(stale (expand-file-name server-name socket-dir)))
(when (file-exists-p stale)
(delete-file stale)
(message "[post-init] Cleaned stale daemon socket: %s" stale)))
(error nil))))
;; Add the local lisp directory to Emacs' load path using the true root directory
;; (not user-emacs-directory, since we changed that to var/)
(add-to-list 'load-path (expand-file-name "lisp" minimal-emacs-user-directory))
;; Set fringe width to match character size — scales with font, avoids fixed-width gap.
(fringe-mode (frame-char-width))
;; Typed text replaces active selection (standard behavior in every other editor).
(delete-selection-mode 1)
;; Visual column ruler at 80 characters — helps enforce line length discipline.
(global-display-fill-column-indicator-mode 1)
;; Smooth pixel-level scrolling (reduces jitter on macOS).
(when (fboundp 'pixel-scroll-precision-mode)
(setq pixel-scroll-precision-use-momentum nil)
(pixel-scroll-precision-mode 1))
;; Maximum tree-sitter syntax highlighting depth for richer colors.
(setq treesit-font-lock-level 4)
;; Load the modular configuration files
(require 'init-system)
(require 'init-completion)
(require 'init-evil)
(require 'init-dev)
(require 'init-tools)
(require 'init-org) ; Org mode configuration
;; Defer AI module loading for workflow daemons to break C stack overflow.
;; run-with-timer 0 processes from event loop with clean stack.
(if (string= (getenv "MINIMAL_EMACS_WORKFLOW_DAEMON") "1")
;; Defer AI loading to break C stack overflow. Wrap in with-temp-message
;; to serialize all message writes — prevents *Messages* buffer corruption
;; from interleaved load/init messages during concurrent module initialization.
(run-at-time 0.5 nil
(lambda ()
(condition-case err
(let ((load-verbose nil)
(inhibit-message t))
(require 'init-ai)
;; Force-reload key modules so chain-backend selection,
;; OR-condition override, and nil-backend failover are
;; available. load-file always re-evaluates from source.
(dolist (mod '("gptel-tools-agent-error"
"gptel-tools-agent-prompt-build"
"gptel-benchmark-subagent"))
(load-file (expand-file-
8000
name
(format "lisp/modules/%s.el" mod)
minimal-emacs-user-directory)))
(load (expand-file-name "lisp/modules/standalone-research.el"
minimal-emacs-user-directory) nil 'nomessage)
(when (fboundp 'slr-run-research)
(defalias 'gptel-auto-workflow-run-research 'slr-run-research)))
(error
(message "[init-daemon] Deferred init-ai loading error: %s"
(error-message-string err)))))))
(require 'init-ai)
;; Ensure config-verified flag even if init.el errors before its line 641.
;; Must be set after all module loads but before any post-init code that
;; might error and abort loading (which prevents init.el:641 from running).
(setq minimal-emacs--success t)
;; Backup and auto-save settings are configured in init-files.el
;; ==============================================================================
;; DEBUG: Auto-capture backtrace for mode-line errors
;; ==============================================================================
;; "Wrong type argument: stringp, nil" occurs during mode-line updates.
;; Catch it in format-mode-line and log a full backtrace.
(defvar my/backtrace-log
(expand-file-name "var/log/backtrace-mode-line.log" user-emacs-directory))
(defun my/format-mode-line-backtrace (orig-fn format &optional face window buffer)
(condition-case err
(funcall orig-fn format face window buffer)
(wrong-type-argument
(when (string-match-p "stringp, nil" (error-message-string err))
(with-temp-file my/backtrace-log
(prin1 (format-time-string "%Y-%m-%dT%T ") (current-buffer))
(prin1 err (current-buffer))
(terpri (current-buffer))
(let ((standard-output (current-buffer))
(debug-on-error nil))
(backtrace))))
(signal (car err) (cdr err)))))
(advice-add 'format-mode-line :around #'my/format-mode-line-backtrace)
;; ==============================================================================
;; PERSONAL CUSTOMIZATIONS (Add your own below)
;; ==============================================================================
;; Example: Set your preferred theme
;; (load-theme 'doom-one t)
;; Example: Custom key bindings
;; (global-set-key (kbd "C-c w") #'whitespace-mode)
;; ==============================================================================
;; FIX: Mode-line restoration for buffers created during startup
;; ==============================================================================
;; Upstream early-init.el only restores mode-line for buffers that had
;; minimal-emacs--hidden-mode-line set. Buffers created during startup
;; with nil mode-line are skipped. This fixes that.
(defun my/fix-mode-line-for-all-buffers ()
"Ensure all buffers have a proper mode-line-format.
Buffers created during startup may have nil mode-line if they
were created after the hiding but before restoration."
(when (and (boundp 'minimal-emacs-disable-mode-line-during-startup)
minimal-emacs-disable-mode-line-during-startup)
(dolist (buf (buffer-list))
(when (buffer-live-p buf)
(with-current-buffer buf
;; Buffer has nil mode-line as local var - restore to default
(when (and (local-variable-p 'mode-line-format)
(eq mode-line-format nil))
(kill-local-variable 'mode-line-format)))))))
;; --- Auto-workflow: register external projects ---
(let ((projects-f (expand-file-name "lisp/modules/gptel-auto-workflow-projects.el"
user-emacs-directory))
(base-f (expand-file-name "lisp/modules/gptel-tools-agent-base.el"
user-emacs-directory))
(creatoros-dir (expand-file-name "~/workspace/creatoros/")))
(when (file-directory-p creatoros-dir)
;; Step 1: register as project (lightweight — no deps needed)
(condition-case nil
(when (and (file-exists-p projects-f) (load-file projects-f))
(add-to-list 'gptel-auto-workflow-projects
(directory-file-name creatoros-dir))
(message "[post-init] CreatorOS: project registered"))
(error (message "[post-init] CreatorOS project registration deferred")))
;; Step 2: allow workspace boundary (heavier deps — may fail)
(condition-case nil
(when (and (file-exists-p base-f) (load-file base-f))
(add-to-list 'gptel-auto-workflow--allowed-workspace-roots creatoros-dir)
(message "[post-init] CreatorOS: workspace boundary allowed"))
(error (message "[post-init] CreatorOS boundary deferred — will retry on first workflow run")))))
;; Run after all init is complete
(add-hook 'emacs-startup-hook #'my/fix-mode-line-for-all-buffers 100)
;; Retry workspace boundary registration after full init (deps now loaded)
(add-hook 'emacs-startup-hook
(lambda ()
(let* ((base-f (expand-file-name "lisp/modules/gptel-tools-agent-base.el"
user-emacs-directory))
(creatoros-dir (expand-file-name "~/workspace/creatoros/")))
(when (and (file-directory-p creatoros-dir)
(file-exists-p base-f)
(not (member creatoros-dir gptel-auto-workflow--allowed-workspace-roots)))
(condition-case nil
(progn
(load-file base-f)
(add-to-list 'gptel-auto-workflow--allowed-workspace-roots creatoros-dir)
(message "[post-init] CreatorOS boundary: now allowed"))
(error (message "[post-init] CreatorOS boundary still unavailable"))))))
90)
;;; post-init.el ends here
;; ==============================================================================
;; RELOAD THEME-SETTING.EL FOR NEW GUI FRAMES
;; ==============================================================================
(defun my/reload-theme-setting-for-frame (frame)
"Rel
8000
oad theme-setting.el for FRAME to apply all visual settings.
Only reloads for top-level frames (not Corfu child frames) and only once per frame."
(when (and (display-graphic-p frame)
(not (frame-parameter frame 'parent-frame))
(not (frame-parameter frame 'theme-setting-loaded)))
(set-frame-parameter frame 'theme-setting-loaded t)
(select-frame frame)
(load-file "~/.emacs.d/lisp/theme-setting.el")
(message "✅ Reloaded theme-setting.el for new frame")))
;; Apply all settings from theme-setting.el to every new GUI frame
(add-hook 'after-make-frame-functions #'my/reload-theme-setting-for-frame)
;; Load theme initially for non-daemon mode
(unless (daemonp)
(load-file "~/.emacs.d/lisp/theme-setting.el"))
;; ─── Workflow Daemon: handled by deferred init-ai loading above ───
;; ─── Server socket self-healing ───
;; During long gptel-curl LLM calls, the emacs server socket can be lost
;; (network timeout, pipe break) while the Emacs process survives. Instead
;; of requiring an external watchdog to SIGKILL and restart (losing all
;; in-progress experiment work), recreate the socket in-process every 30s.
;; Also ensure server-start is called initially if not already done.
(when (daemonp)
;; The --daemon flag already starts the server after init files load.
;; Do NOT call `server-start' here: at this point `server-process' is not
;; yet bound, so `server-start' sees a stale/external server, stops it,
;; and restarts, which reloads init files and can race itself into an
;; "End of file during parsing" loop on macOS.
(message "[server] Daemon server will be started by --daemon flag after init")
;; Start the heartbeat timer at daemon init so it ALWAYS runs while the
;; daemon lives (not only during an active workflow). The external
;; watchdog (watchdog-daemon.sh) treats a stale heartbeat as "frozen";
;; if the heartbeat only starts mid-workflow, an idle daemon has a
;; forever-stale heartbeat and gets force-restarted every 30 min even
;; when it is merely briefly busy (a >5s git/test op causes the 5s
;; emacsclient ping to miss, and with no fresh heartbeat the watchdog
;; concludes "frozen"). A live event loop keeps this timer firing
;; every 30s; a truly frozen main thread starves it → stale → watchdog
;; restarts. That is the correct invariant.
;; Eager-require the module so the heartbeat starts immediately at init
;; instead of waiting for its first lazy load (which can race the
;; watchdog's first 30-min check and cause a false "stale" restart).
(condition-case err
(progn
(require 'gptel-tools-agent-experiment-loop)
(when (fboundp 'gptel-auto-workflow--start-heartbeat-timer)
(gptel-auto-workflow--start-heartbeat-timer)))
(error
(message "[heartbeat] Init heartbeat failed: %s" (error-message-string err))))
;; Hard curl timeout prevents orphaned subprocesses from hanging the daemon.
;; Without --max-time, curl blocks indefinitely when server closes connection
;; without FIN, filling the 64KB pipe buffer on macOS and never exiting.
;; Must use with-eval-after-load because gptel is loaded lazily; boundp
;; returns nil during post-init startup.
(with-eval-after-load 'gptel-request
(add-to-list 'gptel-curl-extra-args "--max-time" t)
(add-to-list 'gptel-curl-extra-args "840" t)
(message "[post-init] Set gptel curl --max-time 840s for daemon"))
(defun my/server-socket-healthy-p (sock)
"Return non-nil if the server socket SOCK is live.
Checks the in-process `server-process' rather than round-tripping
through emacsclient. A synchronous `call-process' that invokes
emacsclient back into THIS SAME DAEMON self-deadlocks: `call-process'
blocks the main thread waiting for the child, so the server loop never
services the very request the child just sent — both wait forever.
This deadlocked the daemon ~30s after every startup (the self-heal
timer fired at t+30s and froze the main thread), which in turn made the
external watchdog restart the daemon every 30 minutes in a loop."
(and (boundp 'server-process)
(processp server-process)
(process-live-p server-process)
(file-exists-p sock)))
;; Self-heal: check every 30s and recreate socket if unreachable
(run-at-time 30 30
(lambda ()
(when (and (boundp 'server-name) (stringp server-name)
(boundp 'server-socket-dir) (stringp server-socket-dir))
(let ((sock (expand-file-name server-name server-socket-dir)))
(unless (my/server-socket-healthy-p sock)
(message "[server] Socket unhealthy: %s, attempting self-heal" sock)
(when server-process
(condition-case nil (delete-process server-process) (error nil))
(setq server-process nil))
(condition-case nil (delete-file sock) (error nil))
(condition-case err
(progn (server-start)
(message "[server] Self-healed socket %s" sock))
(error
(message "[server] Self-heal failed: %s"
(error-message-string err))))))))))
;; ─── Critical function corruption guard ───
;; Self-heal: if pending-decisions-p returns a symbol instead of t/nil,
;; the entire pipeline is blocked. Redefine from source if corrupted.
;; This can happen when auto-evolution introduces subtle paren imbalance
;; that shifts function definitions.
(add-hook 'emacs-startup-hook
(lambda ()
(when (and (fboundp 'gptel-auto-workflow--pending-decisions-p)
(not (daemonp)))
;; Skip check in daemon — daemon loads this before production.el
t)
(when (and (fboundp 'gptel-auto-workflow--pending-decisions-p)
(daemonp))
(let ((result (condition-case nil
(gptel-auto-workflow--pending-decisions-p)
(error 'ERR))))
(when (and (not (eq result t)) (not (eq result nil))
(not (eq result 'ERR)))
(message "[CRITICAL] pending-decisions-p corrupted (returns %s), reloading production.el" result)
352A
(load "lisp/modules/gptel-auto-workflow-production.el" t t)
(let ((result2 (condition-case nil
(gptel-auto-workflow--pending-decisions-p)
(error 'ERR))))
(if (or (eq result2 t) (eq result2 nil))
(message "[CRITICAL] Fixed pending-decisions-p after reload")
(message "[CRITICAL] FAILED to fix pending-decisions-p (still returns %s)" result2))))))))