zjcode 0.0.1__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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Session-end summary writer.
|
|
2
|
+
|
|
3
|
+
Records why a dcode session ended (`completed` / `interrupted` / `error`) and
|
|
4
|
+
how long it ran, then emits the result to three sinks on shutdown:
|
|
5
|
+
|
|
6
|
+
1. `stderr` — a human-readable panel printed after all UI teardown.
|
|
7
|
+
2. `~/.deepagents/.state/session_end/<thread_id>.txt` — one file per session
|
|
8
|
+
(last-write-wins) for post-mortem inspection.
|
|
9
|
+
3. `~/.deepagents/session_end_log.jsonl` — one JSON line appended per session
|
|
10
|
+
for audit/aggregation.
|
|
11
|
+
|
|
12
|
+
Design constraints:
|
|
13
|
+
- **Minimal-invasive**: this module is self-contained. It hooks into shutdown
|
|
14
|
+
via `atexit` + `sys.excepthook`, so mainline code touches only
|
|
15
|
+
`install(...)`, `set_thread_id(...)`, `mark_reason(...)`, and
|
|
16
|
+
`mark_signal(...)` — small entry points that don't collide with upstream
|
|
17
|
+
evolution of `app.py` or `client/non_interactive.py`.
|
|
18
|
+
- **Idempotent**: `_finalize()` may fire multiple times; a lock + `_finalized`
|
|
19
|
+
flag makes only the first invocation write anything.
|
|
20
|
+
- **Never raises**: any I/O or formatting failure is logged at debug level and
|
|
21
|
+
swallowed. Failing to write the summary must never mask the real exit code
|
|
22
|
+
or crash the shutdown path.
|
|
23
|
+
- **Zero heavy imports at import time**: pulls `format_duration` lazily inside
|
|
24
|
+
`_finalize` so this module stays cheap for the `-v` fast path.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import atexit
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import signal
|
|
34
|
+
import sys
|
|
35
|
+
import threading
|
|
36
|
+
import time
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import TYPE_CHECKING
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from types import TracebackType
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
REASON_COMPLETED = "completed"
|
|
47
|
+
"""Normal exit — `/exit`, Ctrl-D, end of script, `sys.exit(0)`, etc."""
|
|
48
|
+
|
|
49
|
+
REASON_INTERRUPTED = "interrupted"
|
|
50
|
+
"""User or OS terminated the process — Ctrl-C, SIGTERM, SIGHUP, SIGQUIT."""
|
|
51
|
+
|
|
52
|
+
REASON_ERROR = "error"
|
|
53
|
+
"""An unhandled exception propagated out of the program."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_state_lock = threading.Lock()
|
|
57
|
+
"""Guards all mutable module-level state so mark/finalize races are safe."""
|
|
58
|
+
|
|
59
|
+
_installed = False
|
|
60
|
+
"""True after `install()` has run so double-install is a no-op."""
|
|
61
|
+
|
|
62
|
+
_finalized = False
|
|
63
|
+
"""True after `_finalize()` has emitted output so re-entry is a no-op."""
|
|
64
|
+
|
|
65
|
+
_start_monotonic: float | None = None
|
|
66
|
+
"""`time.monotonic()` at install time; used to compute wall-clock duration."""
|
|
67
|
+
|
|
68
|
+
_thread_id: str = ""
|
|
69
|
+
"""Session/thread id, populated by `set_thread_id` when known."""
|
|
70
|
+
|
|
71
|
+
_reason: str = REASON_COMPLETED
|
|
72
|
+
"""Current best guess at the exit reason; upgraded by `mark_reason`."""
|
|
73
|
+
|
|
74
|
+
_reason_detail: str = ""
|
|
75
|
+
"""Optional short human-readable detail (e.g. `SIGINT`, `RuntimeError: foo`)."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Upgrade order — higher wins. This lets late-arriving signals overwrite an
|
|
79
|
+
# earlier `completed`, but a first-seen `error` isn't clobbered by a later
|
|
80
|
+
# atexit-triggered `completed`.
|
|
81
|
+
_REASON_PRIORITY = {
|
|
82
|
+
REASON_COMPLETED: 0,
|
|
83
|
+
REASON_INTERRUPTED: 1,
|
|
84
|
+
REASON_ERROR: 2,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_SIGNAL_NAMES: dict[int, str] = {}
|
|
89
|
+
for _name in ("SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"):
|
|
90
|
+
_sig = getattr(signal, _name, None)
|
|
91
|
+
if _sig is not None:
|
|
92
|
+
_SIGNAL_NAMES[int(_sig)] = _name
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def install(*, thread_id: str = "") -> None:
|
|
96
|
+
"""Register shutdown hooks and record the session start time.
|
|
97
|
+
|
|
98
|
+
Safe to call more than once; subsequent calls are no-ops except that a
|
|
99
|
+
non-empty `thread_id` will fill an empty slot.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
thread_id: Best-known session/thread identifier at install time. May
|
|
103
|
+
be updated later via `set_thread_id` once the client learns the
|
|
104
|
+
real id.
|
|
105
|
+
"""
|
|
106
|
+
global _installed, _start_monotonic
|
|
107
|
+
|
|
108
|
+
with _state_lock:
|
|
109
|
+
if not _installed:
|
|
110
|
+
_start_monotonic = time.monotonic()
|
|
111
|
+
_installed = True
|
|
112
|
+
# atexit fires for normal termination *and* for `sys.exit()`, and
|
|
113
|
+
# runs after Textual/Rich have torn down, so our stderr print is
|
|
114
|
+
# not swallowed by an alt-screen. It does NOT fire for `os._exit`,
|
|
115
|
+
# `kill -9`, or a fatal signal that bypasses Python — none of
|
|
116
|
+
# which can be caught anywhere in userspace anyway.
|
|
117
|
+
atexit.register(_finalize)
|
|
118
|
+
_install_excepthook()
|
|
119
|
+
|
|
120
|
+
if thread_id and not _thread_id:
|
|
121
|
+
_set_thread_id_locked(thread_id)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _install_excepthook() -> None:
|
|
125
|
+
"""Chain a classification wrapper on top of the existing `sys.excepthook`."""
|
|
126
|
+
prev_hook = sys.excepthook
|
|
127
|
+
|
|
128
|
+
def _hook(
|
|
129
|
+
exc_type: type[BaseException],
|
|
130
|
+
exc: BaseException,
|
|
131
|
+
tb: TracebackType | None,
|
|
132
|
+
) -> None:
|
|
133
|
+
# KeyboardInterrupt reaches excepthook when it escapes the main frame;
|
|
134
|
+
# classify it as `interrupted`, not `error`.
|
|
135
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
136
|
+
mark_reason(REASON_INTERRUPTED, "KeyboardInterrupt")
|
|
137
|
+
elif issubclass(exc_type, SystemExit):
|
|
138
|
+
# SystemExit is a normal exit path; don't upgrade to error.
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
detail = f"{exc_type.__name__}: {exc}".strip()
|
|
142
|
+
if len(detail) > 500: # noqa: PLR2004
|
|
143
|
+
detail = detail[:497] + "..."
|
|
144
|
+
mark_reason(REASON_ERROR, detail)
|
|
145
|
+
prev_hook(exc_type, exc, tb)
|
|
146
|
+
|
|
147
|
+
sys.excepthook = _hook
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_thread_id(thread_id: str) -> None:
|
|
151
|
+
"""Record the session/thread id once the client learns it."""
|
|
152
|
+
if not thread_id:
|
|
153
|
+
return
|
|
154
|
+
with _state_lock:
|
|
155
|
+
_set_thread_id_locked(thread_id)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _set_thread_id_locked(thread_id: str) -> None:
|
|
159
|
+
"""Thread-id setter that assumes `_state_lock` is held."""
|
|
160
|
+
global _thread_id
|
|
161
|
+
_thread_id = thread_id
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def mark_reason(reason: str, detail: str = "") -> None:
|
|
165
|
+
"""Upgrade the recorded exit reason if `reason` has higher priority.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
reason: One of `REASON_COMPLETED` / `REASON_INTERRUPTED` /
|
|
169
|
+
`REASON_ERROR`. Unknown values are ignored.
|
|
170
|
+
detail: Short human-readable qualifier (e.g. `"SIGINT"`,
|
|
171
|
+
`"RuntimeError: foo"`).
|
|
172
|
+
"""
|
|
173
|
+
if reason not in _REASON_PRIORITY:
|
|
174
|
+
return
|
|
175
|
+
global _reason, _reason_detail
|
|
176
|
+
with _state_lock:
|
|
177
|
+
if _REASON_PRIORITY[reason] >= _REASON_PRIORITY[_reason]:
|
|
178
|
+
_reason = reason
|
|
179
|
+
if detail:
|
|
180
|
+
_reason_detail = detail
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def mark_signal(signum: int) -> None:
|
|
184
|
+
"""Record that a terminating signal was received.
|
|
185
|
+
|
|
186
|
+
Called from `main.py`'s signal handler right before it raises
|
|
187
|
+
`SystemExit`. `SystemExit` itself won't route through `sys.excepthook`, so
|
|
188
|
+
this is the only chance to attach a signal-derived detail.
|
|
189
|
+
"""
|
|
190
|
+
name = _SIGNAL_NAMES.get(signum, f"signal {signum}")
|
|
191
|
+
mark_reason(REASON_INTERRUPTED, name)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _summary_dir() -> Path:
|
|
195
|
+
"""`~/.deepagents/.state/session_end/`, created lazily and idempotently."""
|
|
196
|
+
from deepagents_code.model_config import DEFAULT_STATE_DIR
|
|
197
|
+
|
|
198
|
+
path = DEFAULT_STATE_DIR / "session_end"
|
|
199
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
return path
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _log_path() -> Path:
|
|
204
|
+
"""`~/.deepagents/session_end_log.jsonl`, parent created if missing."""
|
|
205
|
+
from deepagents_code.model_config import DEFAULT_CONFIG_DIR
|
|
206
|
+
|
|
207
|
+
DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
return DEFAULT_CONFIG_DIR / "session_end_log.jsonl"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _format_duration(seconds: float) -> str:
|
|
212
|
+
"""Format a duration; try `formatting.format_duration`, fall back locally."""
|
|
213
|
+
try:
|
|
214
|
+
from deepagents_code.formatting import format_duration
|
|
215
|
+
|
|
216
|
+
return format_duration(seconds)
|
|
217
|
+
except Exception:
|
|
218
|
+
total = int(seconds)
|
|
219
|
+
h, rem = divmod(total, 3600)
|
|
220
|
+
m, s = divmod(rem, 60)
|
|
221
|
+
if h:
|
|
222
|
+
return f"{h}h{m:02d}m{s:02d}s"
|
|
223
|
+
if m:
|
|
224
|
+
return f"{m}m{s:02d}s"
|
|
225
|
+
return f"{s}s"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _sanitize_filename(name: str) -> str:
|
|
229
|
+
"""Strip path separators and other unsafe characters from a thread id."""
|
|
230
|
+
safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in name)
|
|
231
|
+
return safe or "unknown"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _print_stderr_panel(
|
|
235
|
+
reason_line: str,
|
|
236
|
+
duration_pretty: str,
|
|
237
|
+
thread_id: str,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Print the human-facing summary to stderr as a plain-text panel."""
|
|
240
|
+
log_hint = "~/.deepagents/session_end_log.jsonl"
|
|
241
|
+
lines = [
|
|
242
|
+
"",
|
|
243
|
+
"─────────── Session ended ───────────",
|
|
244
|
+
f"Reason: {reason_line}",
|
|
245
|
+
f"Duration: {duration_pretty}",
|
|
246
|
+
f"Thread: {thread_id}",
|
|
247
|
+
f"Log: {log_hint}",
|
|
248
|
+
"──────────────────────────────────────",
|
|
249
|
+
"",
|
|
250
|
+
]
|
|
251
|
+
sys.stderr.write("\n".join(lines))
|
|
252
|
+
sys.stderr.flush()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _finalize() -> None:
|
|
256
|
+
"""Emit the session-end summary to all three sinks. Idempotent."""
|
|
257
|
+
global _finalized
|
|
258
|
+
|
|
259
|
+
with _state_lock:
|
|
260
|
+
if _finalized or not _installed:
|
|
261
|
+
return
|
|
262
|
+
_finalized = True
|
|
263
|
+
reason = _reason
|
|
264
|
+
detail = _reason_detail
|
|
265
|
+
thread_id = _thread_id or "<unknown>"
|
|
266
|
+
started = _start_monotonic
|
|
267
|
+
|
|
268
|
+
duration = time.monotonic() - started if started is not None else 0.0
|
|
269
|
+
duration_pretty = _format_duration(duration)
|
|
270
|
+
reason_line = reason if not detail else f"{reason} ({detail})"
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
_print_stderr_panel(reason_line, duration_pretty, thread_id)
|
|
274
|
+
except Exception:
|
|
275
|
+
logger.debug("session_end_summary stderr print failed", exc_info=True)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
safe_name = _sanitize_filename(thread_id)
|
|
279
|
+
text_path = _summary_dir() / f"{safe_name}.txt"
|
|
280
|
+
text_path.write_text(
|
|
281
|
+
"Session ended\n"
|
|
282
|
+
f"Reason: {reason_line}\n"
|
|
283
|
+
f"Duration: {duration_pretty} ({duration:.3f}s)\n"
|
|
284
|
+
f"Thread: {thread_id}\n"
|
|
285
|
+
f"PID: {os.getpid()}\n",
|
|
286
|
+
encoding="utf-8",
|
|
287
|
+
)
|
|
288
|
+
except Exception:
|
|
289
|
+
logger.debug("session_end_summary per-thread write failed", exc_info=True)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
record = {
|
|
293
|
+
"ts": time.time(),
|
|
294
|
+
"thread_id": thread_id,
|
|
295
|
+
"pid": os.getpid(),
|
|
296
|
+
"reason": reason,
|
|
297
|
+
"reason_detail": detail,
|
|
298
|
+
"duration_seconds": round(duration, 3),
|
|
299
|
+
"duration_pretty": duration_pretty,
|
|
300
|
+
}
|
|
301
|
+
# `a` + `write(line + "\n")` in a single call is atomic on POSIX for
|
|
302
|
+
# writes below PIPE_BUF; the JSON line is well under that limit.
|
|
303
|
+
with _log_path().open("a", encoding="utf-8") as fp:
|
|
304
|
+
fp.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
305
|
+
except Exception:
|
|
306
|
+
logger.debug("session_end_summary JSONL append failed", exc_info=True)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _reset_for_tests() -> None:
|
|
310
|
+
"""Reset all module state. Test-only; not part of the public API.
|
|
311
|
+
|
|
312
|
+
Also unregisters the `atexit` handler installed by `install()` so pytest
|
|
313
|
+
exit does not print a stray summary panel from a prior test's install.
|
|
314
|
+
Tests that want the hooks re-armed should call `install()` again on a
|
|
315
|
+
fresh clock.
|
|
316
|
+
"""
|
|
317
|
+
global _installed, _finalized, _start_monotonic, _thread_id, _reason
|
|
318
|
+
global _reason_detail
|
|
319
|
+
try:
|
|
320
|
+
atexit.unregister(_finalize)
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
with _state_lock:
|
|
324
|
+
_installed = False
|
|
325
|
+
_finalized = False
|
|
326
|
+
_start_monotonic = None
|
|
327
|
+
_thread_id = ""
|
|
328
|
+
_reason = REASON_COMPLETED
|
|
329
|
+
_reason_detail = ""
|