Skip to content

Commit 5f5421e

Browse files
committed
Release CodexTools 1.2.9
1 parent 6c860f7 commit 5f5421e

45 files changed

Lines changed: 1480 additions & 246 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎assets/inject/renderer-inject.js‎

Lines changed: 422 additions & 151 deletions
Large diffs are not rendered by default.

‎bridge.go‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,19 @@ func (r *launcherRuntime) handleBridgeRequest(path string, payload json.RawMessa
2020
started := time.Now()
2121
var payloadMap map[string]any
2222
_ = json.Unmarshal(payload, &payloadMap)
23-
appendDiagnosticLog("bridge.request", map[string]any{"path": path, "payload_keys": mapKeys(payloadMap)})
23+
trace := shouldTraceBridgePath(path)
24+
if trace {
25+
appendDiagnosticLog("bridge.request", map[string]any{"path": path, "payload_keys": mapKeys(payloadMap)})
26+
}
2427
var result map[string]any
2528
switch path {
2629
case "/backend/status", "/backend/repair":
27-
active := activeRelayProfile(loadSettings())
30+
settings := r.runtimeSettingsSnapshot()
31+
if strings.TrimSpace(settings.Language) == "" {
32+
settings = loadSettings()
33+
r.setRuntimeSettings(settings)
34+
}
35+
active := activeRelayProfile(settings)
2836
result = map[string]any{
2937
"status": "ok",
3038
"message": "后端已连接",
@@ -41,6 +49,8 @@ func (r *launcherRuntime) handleBridgeRequest(path string, payload json.RawMessa
4149
case "/diagnostics/log":
4250
r.logRendererDiagnostic(payload)
4351
result = map[string]any{"status": "ok", "message": "日志已记录"}
52+
case "/diagnostics/runtime":
53+
result = runtimeDiagnosticsValue()
4454
case "/user-scripts/list":
4555
result = userScriptInventoryValue(payloadMap["runtime_status"])
4656
case "/user-scripts/set-enabled":
@@ -144,14 +154,26 @@ func (r *launcherRuntime) handleBridgeRequest(path string, payload json.RawMessa
144154
result = map[string]any{"status": "failed", "message": "Unknown bridge path", "path": path}
145155
appendDiagnosticLog("bridge.unknown_path", map[string]any{"path": path})
146156
}
147-
appendDiagnosticLog("bridge.response", map[string]any{
148-
"path": path,
149-
"elapsed_ms": time.Since(started).Milliseconds(),
150-
"status": stringFromAny(result["status"]),
151-
})
157+
status := stringFromAny(result["status"])
158+
if trace || status != "" && status != "ok" || time.Since(started) >= time.Second {
159+
appendDiagnosticLog("bridge.response", map[string]any{
160+
"path": path,
161+
"elapsed_ms": time.Since(started).Milliseconds(),
162+
"status": status,
163+
})
164+
}
152165
return result
153166
}
154167

168+
func shouldTraceBridgePath(path string) bool {
169+
switch path {
170+
case "/diagnostics/log", "/diagnostics/runtime", "/backend/status", "/backend/repair", "/settings/get", "/realtime/status", "/thread-sort-key", "/thread-sort-keys":
171+
return false
172+
default:
173+
return true
174+
}
175+
}
176+
155177
func mapKeys(value map[string]any) []string {
156178
keys := make([]string, 0, len(value))
157179
for key := range value {

‎bridge_parity_test.go‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,74 @@ func TestBridgeParityRoutesExistAndAdsStayAbsent(t *testing.T) {
174174
}
175175
}
176176

177+
func TestHighFrequencyBridgeDiagnosticsAreSuppressed(t *testing.T) {
178+
t.Setenv("HOME", t.TempDir())
179+
runtime := &launcherRuntime{}
180+
181+
runtime.handleBridgeRequest("/settings/get", json.RawMessage(`{}`))
182+
runtime.handleBridgeRequest("/diagnostics/log", json.RawMessage(`{"event":"performance_test","detail":{}}`))
183+
logs := readFile(diagnosticLogPath())
184+
if strings.Contains(logs, `"event":"bridge.request"`) || strings.Contains(logs, `"event":"bridge.response"`) {
185+
t.Fatalf("high-frequency bridge routes should not emit request/response trace logs: %s", logs)
186+
}
187+
if !strings.Contains(logs, `"event":"renderer.performance_test"`) {
188+
t.Fatalf("renderer diagnostic should still be persisted: %s", logs)
189+
}
190+
for _, path := range []string{"/settings/get", "/thread-sort-keys", "/diagnostics/log", "/diagnostics/runtime", "/v1/responses", "/v1/live"} {
191+
if shouldTraceHelperRequest(path) {
192+
t.Fatalf("helper request path %s should be quiet", path)
193+
}
194+
}
195+
if !shouldTraceHelperRequest("/delete") {
196+
t.Fatal("session delete should retain a diagnostic trace")
197+
}
198+
}
199+
200+
func TestBackendHeartbeatUsesRuntimeSettingsSnapshot(t *testing.T) {
201+
t.Setenv("HOME", t.TempDir())
202+
cached := defaultSettings()
203+
cached.ActiveRelayID = "cached"
204+
cached.RelayProfiles = []relayProfile{{ID: "cached", HideOfficialUsageAlert: true}}
205+
onDisk := cached
206+
onDisk.RelayProfiles = []relayProfile{{ID: "cached", HideOfficialUsageAlert: false}}
207+
if err := saveSettings(onDisk); err != nil {
208+
t.Fatalf("save disk settings: %v", err)
209+
}
210+
211+
result := (&launcherRuntime{settings: cached}).handleBridgeRequest("/backend/status", json.RawMessage(`{}`))
212+
if !boolFromAny(result["hideOfficialUsageAlert"]) {
213+
t.Fatalf("backend heartbeat should use runtime settings without rereading disk: %#v", result)
214+
}
215+
}
216+
217+
func TestInjectionPerformanceGuardsArePresent(t *testing.T) {
218+
script, err := os.ReadFile(filepath.Join("assets", "inject", "renderer-inject.js"))
219+
if err != nil {
220+
t.Fatalf("read renderer injection failed: %v", err)
221+
}
222+
source := string(script)
223+
for _, marker := range []string{
224+
"codexAppAssetCandidateUrlsCacheTtlMs = 1000",
225+
"appServerRequestCandidatesCacheTtlMs = 60000",
226+
"codexModelCatalogCacheTtlMs = 60000",
227+
"pluginMarketplaceRequestPatchRetryDelaysMs = [500, 1500, 3000, 6000, 12000, 30000]",
228+
"appServerModelRequestPatchRetryDelaysMs = [250, 500, 1000, 2000, 4000, 8000, 15000]",
229+
"appServerModelRequestPatchMissCount >= appServerModelRequestPatchMaxMisses",
230+
"typeof window.__codexSessionDeleteBridge === \"function\"",
231+
"chatsSortRefreshIntervalMs = 5000",
232+
"chatsSortDbRefreshIntervalMs = 20000",
233+
"codexPlusBackendHeartbeatVisibleIntervalMs = 15000",
234+
"codexPlusBackendHeartbeatHiddenIntervalMs = 60000",
235+
"scheduleConversationViewAlign(1), 2000",
236+
"conversationViewReleaseElement",
237+
"rememberCodexRuntimeFailure",
238+
} {
239+
if !strings.Contains(source, marker) {
240+
t.Fatalf("renderer injection missing performance guard %q", marker)
241+
}
242+
}
243+
}
244+
177245
func TestBridgeSettingsIncludesRuntimeCodexAppVersion(t *testing.T) {
178246
t.Setenv("HOME", t.TempDir())
179247
runtime := &launcherRuntime{codexAppPath: filepath.Join("C:", "Program Files", "WindowsApps", "OpenAI.ChatGPT_26.601.2237.0_x64__2p2nqsd0c76g0", "app")}

‎diagnostics.go‎

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package main
22

33
import (
4+
"bytes"
45
"encoding/json"
6+
"io"
57
"net/url"
68
"os"
79
"path/filepath"
@@ -13,6 +15,22 @@ import (
1315

1416
var diagnosticThrottle sync.Map
1517

18+
const (
19+
diagnosticLogMaxBytes = 32 << 20
20+
diagnosticLogReadChunk = 64 << 10
21+
diagnosticLogMaxTailBytes = 4 << 20
22+
diagnosticLogMaxReadLines = 5000
23+
)
24+
25+
var diagnosticLogWriteMu sync.Mutex
26+
var diagnosticLogStates = map[string]*diagnosticLogState{}
27+
28+
type diagnosticLogState struct {
29+
size int64
30+
writes uint64
31+
initialized bool
32+
}
33+
1634
func appendDiagnosticLog(event string, detail map[string]any) {
1735
if detail == nil {
1836
detail = map[string]any{}
@@ -35,12 +53,60 @@ func appendDiagnosticLog(event string, detail map[string]any) {
3553
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
3654
return
3755
}
56+
57+
diagnosticLogWriteMu.Lock()
58+
defer diagnosticLogWriteMu.Unlock()
59+
state := diagnosticLogStates[path]
60+
if state == nil {
61+
state = &diagnosticLogState{}
62+
diagnosticLogStates[path] = state
63+
}
64+
if !state.initialized {
65+
if info, statErr := os.Stat(path); statErr == nil {
66+
state.size = info.Size()
67+
}
68+
state.initialized = true
69+
}
70+
if state.size+int64(len(data))+1 > diagnosticLogMaxBytes {
71+
state.size = rotateDiagnosticLog(path, state.size)
72+
}
3873
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
3974
if err != nil {
4075
return
4176
}
4277
defer file.Close()
43-
_, _ = file.Write(append(data, '\n'))
78+
n, _ := file.Write(append(data, '\n'))
79+
state.size += int64(n)
80+
state.writes++
81+
// The manager and launcher are separate processes and share this file.
82+
// Re-check occasionally so one process notices writes made by the other.
83+
if state.writes%64 == 0 {
84+
if info, statErr := file.Stat(); statErr == nil {
85+
state.size = info.Size()
86+
}
87+
}
88+
}
89+
90+
func rotateDiagnosticLog(path string, currentSize int64) int64 {
91+
// Do not preserve legacy runaway logs: keeping a multi-gigabyte backup
92+
// would defeat rotation and continue consuming the user's disk.
93+
if currentSize > 2*diagnosticLogMaxBytes {
94+
if err := os.Truncate(path, 0); err == nil {
95+
return 0
96+
}
97+
}
98+
backup := path + ".1"
99+
_ = os.Remove(backup)
100+
if err := os.Rename(path, backup); err == nil || os.IsNotExist(err) {
101+
return 0
102+
}
103+
if err := os.Truncate(path, 0); err == nil {
104+
return 0
105+
}
106+
if info, err := os.Stat(path); err == nil {
107+
return info.Size()
108+
}
109+
return 0
44110
}
45111

46112
func shouldThrottleDiagnosticLog(event string, detail map[string]any, now time.Time) bool {
@@ -182,6 +248,29 @@ func diagnosticSettingsValue(settings backendSettings) any {
182248
}
183249
}
184250

251+
func runtimeDiagnosticsValue() map[string]any {
252+
var memory runtime.MemStats
253+
runtime.ReadMemStats(&memory)
254+
logBytes := int64(0)
255+
if info, err := os.Stat(diagnosticLogPath()); err == nil {
256+
logBytes = info.Size()
257+
}
258+
return map[string]any{
259+
"status": "ok",
260+
"version": version,
261+
"heapAllocBytes": memory.HeapAlloc,
262+
"heapInuseBytes": memory.HeapInuse,
263+
"heapSysBytes": memory.HeapSys,
264+
"stackInuseBytes": memory.StackInuse,
265+
"sysBytes": memory.Sys,
266+
"nextGCBytes": memory.NextGC,
267+
"numGC": memory.NumGC,
268+
"goroutines": runtime.NumGoroutine(),
269+
"diagnosticLogBytes": logBytes,
270+
"diagnosticLogMaxBytes": diagnosticLogMaxBytes,
271+
}
272+
}
273+
185274
func diagnosticAppReference(path string) string {
186275
trimmed := strings.TrimSpace(path)
187276
if trimmed == "" {
@@ -369,12 +458,58 @@ func (s *server) readLatestLogs(args map[string]any) commandResult {
369458
}
370459

371460
func tailFile(path string, maxLines int) (string, error) {
372-
data, err := os.ReadFile(path)
461+
if maxLines <= 0 || maxLines > diagnosticLogMaxReadLines {
462+
maxLines = diagnosticLogMaxReadLines
463+
}
464+
file, err := os.Open(path)
373465
if err != nil {
374466
return "", err
375467
}
468+
defer file.Close()
469+
info, err := file.Stat()
470+
if err != nil {
471+
return "", err
472+
}
473+
if info.Size() == 0 {
474+
return "", nil
475+
}
476+
477+
// Read backwards in bounded chunks. Diagnostics can be large, and the UI
478+
// only needs the most recent lines; loading the entire file caused avoidable
479+
// multi-hundred-megabyte allocations when the log had grown unchecked.
480+
var chunks [][]byte
481+
readBytes := 0
482+
newlines := 0
483+
offset := info.Size()
484+
for offset > 0 && readBytes < diagnosticLogMaxTailBytes && newlines <= maxLines {
485+
start := offset - diagnosticLogReadChunk
486+
if start < 0 {
487+
start = 0
488+
}
489+
if int64(readBytes)+(offset-start) > diagnosticLogMaxTailBytes {
490+
start = offset - int64(diagnosticLogMaxTailBytes-readBytes)
491+
}
492+
chunk := make([]byte, offset-start)
493+
if _, readErr := file.ReadAt(chunk, start); readErr != nil && readErr != io.EOF {
494+
return "", readErr
495+
}
496+
chunks = append(chunks, chunk)
497+
readBytes += len(chunk)
498+
newlines += bytes.Count(chunk, []byte{'\n'})
499+
offset = start
500+
}
501+
data := make([]byte, readBytes)
502+
cursor := 0
503+
for index := len(chunks) - 1; index >= 0; index-- {
504+
cursor += copy(data[cursor:], chunks[index])
505+
}
506+
if offset > 0 {
507+
if newline := bytes.IndexByte(data, '\n'); newline >= 0 {
508+
data = data[newline+1:]
509+
}
510+
}
376511
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
377-
if maxLines > 0 && len(lines) > maxLines {
512+
if len(lines) > maxLines {
378513
lines = lines[len(lines)-maxLines:]
379514
}
380515
return strings.Join(lines, "\n"), nil

0 commit comments

Comments
 (0)