codex-flow 2.1.13__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.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+ import Foundation
2
+
3
+ // TelemetryData.swift references the app localization helper in an unrelated
4
+ // display-only property. Keep this regression test isolated from UI/Combine.
5
+ func L(_ english: String, _ chinese: String) -> String { english }
6
+
7
+ @main
8
+ struct TelemetryQueryEngineConcurrencyTests {
9
+ private struct QuerySnapshot: Equatable {
10
+ let runStems: [String]
11
+ let chatSignatures: [String]
12
+ let filteredRunStems: [String]
13
+ let totalRuns: Int
14
+ let delegatedRuns: Int
15
+ let directRuns: Int
16
+ let totalDurationMs: Double
17
+ let totalTokens: Int
18
+ let parentTokens: Int
19
+ let workerTokens: Int
20
+ let modelSignatures: [String]
21
+ let projectSignatures: [String]
22
+ }
23
+
24
+ private final class FailureRecorder: @unchecked Sendable {
25
+ private let lock = NSLock()
26
+ private var messages: [String] = []
27
+
28
+ func append(_ message: String) {
29
+ lock.lock()
30
+ messages.append(message)
31
+ lock.unlock()
32
+ }
33
+
34
+ var firstMessage: String? {
35
+ lock.lock()
36
+ defer { lock.unlock() }
37
+ return messages.first
38
+ }
39
+ }
40
+
41
+ // DispatchQueue's @Sendable closure cannot prove that this intentionally
42
+ // synchronized reference is safe, so keep the test's crossing explicit.
43
+ private final class EngineBox: @unchecked Sendable {
44
+ let engine: TelemetryQueryEngine
45
+
46
+ init(_ engine: TelemetryQueryEngine) {
47
+ self.engine = engine
48
+ }
49
+ }
50
+
51
+ static func main() {
52
+ testConcurrentQueriesRemainStable()
53
+ print("Telemetry query engine concurrency regression tests passed")
54
+ }
55
+
56
+ private static func testConcurrentQueriesRemainStable() {
57
+ let fileManager = FileManager.default
58
+ let root = fileManager.temporaryDirectory
59
+ .appendingPathComponent("telemetry-query-engine-\(UUID().uuidString)", isDirectory: true)
60
+ let runsDirectory = root.appendingPathComponent("runs", isDirectory: true)
61
+
62
+ do {
63
+ try fileManager.createDirectory(at: runsDirectory, withIntermediateDirectories: true)
64
+ try writeTelemetryRuns(to: runsDirectory)
65
+ } catch {
66
+ preconditionFailure("unable to create telemetry fixtures: \(error)")
67
+ }
68
+ defer { try? fileManager.removeItem(at: root) }
69
+
70
+ let engine = TelemetryQueryEngine(customRoot: root)
71
+ let expected = makeSnapshot(engine: engine, project: "telemetry-alpha")
72
+
73
+ precondition(expected.runStems.count == 12, "fixture should contain 12 runs")
74
+ precondition(expected.chatSignatures.count == 4, "fixture should contain 4 chat sessions")
75
+ precondition(expected.filteredRunStems.count == 6, "project filter should select 6 runs")
76
+ precondition(expected.totalRuns == 12)
77
+ precondition(expected.delegatedRuns == 4)
78
+ precondition(expected.directRuns == 8)
79
+ precondition(expected.totalDurationMs == 12_000)
80
+ precondition(expected.totalTokens == 1_484)
81
+ precondition(expected.parentTokens == 1_266)
82
+ precondition(expected.workerTokens == 218)
83
+ precondition(expected.modelSignatures == [
84
+ "model-parent:12:1266:parent",
85
+ "model-worker:4:218:worker",
86
+ ])
87
+ precondition(expected.projectSignatures == [
88
+ "telemetry-beta:6:748:2",
89
+ "telemetry-alpha:6:736:2",
90
+ ])
91
+
92
+ let engineBox = EngineBox(engine)
93
+ let failures = FailureRecorder()
94
+ let startGate = DispatchGroup()
95
+ let workers = DispatchGroup()
96
+ let workerCount = 8
97
+ let iterations = 30
98
+
99
+ startGate.enter()
100
+ for workerIndex in 0..<workerCount {
101
+ workers.enter()
102
+ DispatchQueue.global(qos: .userInitiated).async {
103
+ defer { workers.leave() }
104
+ startGate.wait()
105
+
106
+ for iteration in 0..<iterations {
107
+ let snapshot = makeSnapshot(
108
+ engine: engineBox.engine,
109
+ project: "telemetry-alpha"
110
+ )
111
+ if snapshot != expected {
112
+ failures.append(
113
+ "worker \(workerIndex), iteration \(iteration) returned an unstable result"
114
+ )
115
+ return
116
+ }
117
+ }
118
+ }
119
+ }
120
+ startGate.leave()
121
+ workers.wait()
122
+
123
+ precondition(
124
+ failures.firstMessage == nil,
125
+ failures.firstMessage ?? "concurrent telemetry query failed"
126
+ )
127
+ }
128
+
129
+ private static func writeTelemetryRuns(to runsDirectory: URL) throws {
130
+ let nowMs = Date().timeIntervalSince1970 * 1000
131
+ let encoder = JSONEncoder()
132
+
133
+ for index in 0..<12 {
134
+ let parentTokens = 100 + index
135
+ let workerTokens = 50 + index
136
+ let sessionIndex = index / 3
137
+ let project = index.isMultiple(of: 2) ? "telemetry-alpha" : "telemetry-beta"
138
+ let workers: [ParticipantInfo]?
139
+ if index.isMultiple(of: 3) {
140
+ workers = [
141
+ ParticipantInfo(
142
+ agentId: "worker-\(index)",
143
+ name: "Worker \(index)",
144
+ agentType: "worker",
145
+ model: "model-worker",
146
+ status: "completed",
147
+ usage: TokenUsage(
148
+ totalTokens: workerTokens,
149
+ promptTokens: 30 + index,
150
+ completionTokens: 20
151
+ )
152
+ )
153
+ ]
154
+ } else {
155
+ workers = nil
156
+ }
157
+
158
+ let run = TaskRun(
159
+ sessionId: "session-\(sessionIndex)",
160
+ turnId: "turn-\(index)",
161
+ startedAtMs: nowMs - Double(12 - index) * 1000,
162
+ finishedAtMs: nowMs - Double(11 - index) * 1000,
163
+ status: "completed",
164
+ thread: ThreadInfo(
165
+ name: "session-\(sessionIndex)",
166
+ preview: "stable telemetry run \(index)",
167
+ cwd: "/tmp/\(project)",
168
+ gitInfo: GitInfo(branch: "main")
169
+ ),
170
+ cwd: "/tmp/\(project)",
171
+ summary: "stable telemetry run \(index)",
172
+ parent: ParticipantInfo(
173
+ name: "Parent",
174
+ agentType: "orchestrator",
175
+ model: "model-parent",
176
+ status: "completed",
177
+ usage: TokenUsage(
178
+ totalTokens: parentTokens,
179
+ promptTokens: parentTokens - 20,
180
+ completionTokens: 20
181
+ )
182
+ ),
183
+ workers: workers
184
+ )
185
+ let data = try encoder.encode(run)
186
+ let fileURL = runsDirectory.appendingPathComponent("run-\(index).json")
187
+ try data.write(to: fileURL, options: .atomic)
188
+ }
189
+ }
190
+
191
+ private static func makeSnapshot(
192
+ engine: TelemetryQueryEngine,
193
+ project: String
194
+ ) -> QuerySnapshot {
195
+ let runs = engine.loadAllRuns()
196
+ let chats = engine.fetchChatHistory(limit: 0)
197
+ let filteredRuns = engine.fetchHistory(limit: 0, project: project)
198
+ let stats = engine.computeStats(days: 30)
199
+
200
+ return QuerySnapshot(
201
+ runStems: runs.map { $0.fileStem ?? "" },
202
+ chatSignatures: chats.map { chat in
203
+ "\(chat.sessionId):\(chat.runs.count):\(chat.latestRun?.fileStem ?? "")"
204
+ },
205
+ filteredRunStems: filteredRuns.map { $0.fileStem ?? "" },
206
+ totalRuns: stats.totalRuns,
207
+ delegatedRuns: stats.delegatedRuns,
208
+ directRuns: stats.directRuns,
209
+ totalDurationMs: stats.totalDurationMs,
210
+ totalTokens: stats.totalTokens,
211
+ parentTokens: stats.parentTokens,
212
+ workerTokens: stats.workerTokens,
213
+ modelSignatures: stats.models.map {
214
+ "\($0.name):\($0.calls):\($0.tokens):\($0.roles.joined(separator: ","))"
215
+ },
216
+ projectSignatures: stats.projects.map {
217
+ "\($0.name):\($0.runs):\($0.tokens):\($0.delegated)"
218
+ }
219
+ )
220
+ }
221
+ }
@@ -0,0 +1,158 @@
1
+ import Foundation
2
+
3
+ // TelemetryData.swift references the app localization helper in an unrelated
4
+ // display-only property. Keep this regression test isolated from UI/Combine.
5
+ func L(_ english: String, _ chinese: String) -> String { english }
6
+
7
+ @main
8
+ struct TelemetryQuotaSelectionTests {
9
+ static func main() {
10
+ testCanonicalDeltaUsesWeeklyWindow()
11
+ testWindowOrderDoesNotChangeSemantics()
12
+ testMissingWeeklyWindowDoesNotFallBackToFiveHour()
13
+ testRemainingMetricsStayExplicit()
14
+ testFallbackDeltaComputedFromBeforeAndAfter()
15
+ print("Telemetry quota selection regression tests passed")
16
+ }
17
+
18
+ private static func testFallbackDeltaComputedFromBeforeAndAfter() {
19
+ let before = [
20
+ QuotaWindow(slot: "primary", usedPercent: 20.0, windowDurationMins: 300),
21
+ QuotaWindow(slot: "secondary", usedPercent: 40.0, windowDurationMins: 10_080)
22
+ ]
23
+ let after = [
24
+ QuotaWindow(slot: "primary", usedPercent: 25.0, windowDurationMins: 300),
25
+ QuotaWindow(slot: "secondary", usedPercent: 46.0, windowDurationMins: 10_080)
26
+ ]
27
+ let run = TaskRun(
28
+ sessionId: "quota-test-fallback",
29
+ turnId: UUID().uuidString,
30
+ status: "completed",
31
+ quotaBefore: before,
32
+ quotaAfter: after
33
+ )
34
+
35
+ precondition(run.shortWindowQuotaDelta == 5.0)
36
+ precondition(run.weeklyQuotaDelta == 6.0)
37
+ precondition(run.observedAccountDelta == 6.0)
38
+ // Contract 1: canonicalQuotaDelta strictly returns allocatedQuotaDelta; nil when missing
39
+ precondition(run.canonicalQuotaDelta == nil)
40
+ let weeklyWindow = run.effectiveQuotaWindows.first { $0.windowDurationMins == 10_080 }
41
+ precondition(weeklyWindow?.deltaPercentagePoints == 6.0)
42
+
43
+ // When allocation is provided, canonicalQuotaDelta returns it
44
+ let runWithAlloc = TaskRun(
45
+ sessionId: "quota-test-alloc",
46
+ turnId: UUID().uuidString,
47
+ status: "completed",
48
+ quotaBefore: before,
49
+ quotaAfter: after,
50
+ allocatedQuotaDelta: 6.0
51
+ )
52
+ precondition(runWithAlloc.canonicalQuotaDelta == 6.0)
53
+ }
54
+
55
+ private static func makeRun(_ windows: [QuotaWindow], allocated: Double? = nil) -> TaskRun {
56
+ TaskRun(
57
+ sessionId: "quota-test",
58
+ turnId: UUID().uuidString,
59
+ status: "completed",
60
+ quotaChangeDuringRun: windows,
61
+ allocatedQuotaDelta: allocated
62
+ )
63
+ }
64
+
65
+ private static func testCanonicalDeltaUsesWeeklyWindow() {
66
+ let runWithoutAlloc = makeRun([
67
+ QuotaWindow(
68
+ slot: "primary",
69
+ usedPercent: 24.0,
70
+ windowDurationMins: 300,
71
+ deltaPercentagePoints: 4.0
72
+ ),
73
+ QuotaWindow(
74
+ slot: "secondary",
75
+ usedPercent: 40.8,
76
+ windowDurationMins: 10_080,
77
+ deltaPercentagePoints: 0.8
78
+ ),
79
+ ])
80
+
81
+ precondition(runWithoutAlloc.shortWindowQuotaDelta == 4.0)
82
+ precondition(runWithoutAlloc.weeklyQuotaDelta == 0.8)
83
+ precondition(runWithoutAlloc.observedAccountDelta == 0.8)
84
+ // Missing allocation strictly returns nil, never falls back to 0.8
85
+ precondition(runWithoutAlloc.canonicalQuotaDelta == nil)
86
+
87
+ // With allocation
88
+ let runWithAlloc = makeRun([
89
+ QuotaWindow(
90
+ slot: "secondary",
91
+ usedPercent: 40.8,
92
+ windowDurationMins: 10_080,
93
+ deltaPercentagePoints: 0.8
94
+ ),
95
+ ], allocated: 0.8)
96
+ precondition(runWithAlloc.canonicalQuotaDelta == 0.8)
97
+ }
98
+
99
+ private static func testWindowOrderDoesNotChangeSemantics() {
100
+ let run = makeRun([
101
+ QuotaWindow(
102
+ slot: "secondary",
103
+ usedPercent: 40.8,
104
+ windowDurationMins: 10_080,
105
+ deltaPercentagePoints: 0.8
106
+ ),
107
+ QuotaWindow(
108
+ slot: "primary",
109
+ usedPercent: 24.0,
110
+ windowDurationMins: 300,
111
+ deltaPercentagePoints: 4.0
112
+ ),
113
+ ], allocated: 0.8)
114
+
115
+ precondition(run.shortWindowQuotaDelta == 4.0)
116
+ precondition(run.weeklyQuotaDelta == 0.8)
117
+ precondition(run.observedAccountDelta == 0.8)
118
+ precondition(run.canonicalQuotaDelta == 0.8)
119
+ }
120
+
121
+ private static func testMissingWeeklyWindowDoesNotFallBackToFiveHour() {
122
+ let run = makeRun([
123
+ QuotaWindow(
124
+ slot: "primary",
125
+ usedPercent: 24.0,
126
+ windowDurationMins: 300,
127
+ deltaPercentagePoints: 4.0
128
+ ),
129
+ ])
130
+
131
+ precondition(run.shortWindowQuotaDelta == 4.0)
132
+ precondition(run.weeklyQuotaDelta == nil)
133
+ precondition(run.canonicalQuotaDelta == nil)
134
+ precondition(run.primaryQuotaDelta == nil)
135
+ }
136
+
137
+ private static func testRemainingMetricsStayExplicit() {
138
+ let run = makeRun([
139
+ QuotaWindow(
140
+ slot: "primary",
141
+ usedPercent: 24.0,
142
+ windowDurationMins: 300,
143
+ deltaPercentagePoints: 4.0
144
+ ),
145
+ QuotaWindow(
146
+ slot: "secondary",
147
+ usedPercent: 40.8,
148
+ windowDurationMins: 10_080,
149
+ deltaPercentagePoints: 0.8
150
+ ),
151
+ ])
152
+
153
+ precondition(run.shortWindowQuotaRemaining == 76.0)
154
+ precondition(run.weeklyQuotaRemaining == 59.2)
155
+ // Preserve the legacy UI meaning of primary remaining as the short window.
156
+ precondition(run.primaryQuotaRemaining == 76.0)
157
+ }
158
+ }
@@ -0,0 +1,122 @@
1
+ import Foundation
2
+
3
+ func L(_ english: String, _ chinese: String) -> String { english }
4
+
5
+ @main
6
+ struct TelemetryWorkerTokenTests {
7
+ static func main() {
8
+ testSingleRunWithWorkers()
9
+ testRunWithZeroWorkers()
10
+ testChatSessionWorkerAggregation()
11
+ print("Telemetry worker token regression tests passed")
12
+ }
13
+
14
+ private static func testSingleRunWithWorkers() {
15
+ let parentUsage = TokenUsage(
16
+ totalTokens: 1_500_000,
17
+ promptTokens: 1_490_000,
18
+ completionTokens: 10_000,
19
+ cachedPromptTokens: 1_400_000,
20
+ reasoningOutputTokens: 5_000
21
+ )
22
+ let parent = ParticipantInfo(
23
+ agentId: "p-1",
24
+ agentType: "parent",
25
+ model: "gpt-5.6-sol",
26
+ usageDelta: parentUsage
27
+ )
28
+
29
+ let workerUsage = TokenUsage(
30
+ totalTokens: 500_000,
31
+ promptTokens: 495_000,
32
+ completionTokens: 5_000,
33
+ cachedPromptTokens: 450_000,
34
+ reasoningOutputTokens: 2_000
35
+ )
36
+ let worker1 = ParticipantInfo(
37
+ agentId: "w-1",
38
+ agentType: "worker-implementer",
39
+ model: "gpt-5.6-luna",
40
+ usage: workerUsage
41
+ )
42
+
43
+ let run = TaskRun(
44
+ sessionId: "sess-1",
45
+ turnId: "turn-1",
46
+ status: "completed",
47
+ parent: parent,
48
+ workers: [worker1]
49
+ )
50
+
51
+ precondition(run.totalTokens == 2_000_000, "Total tokens should be 2.0M, got \(run.totalTokens)")
52
+ precondition(run.workerTotalTokens == 500_000, "Worker tokens should be 500k, got \(run.workerTotalTokens)")
53
+ precondition(run.parentTotalTokens == 1_500_000, "Parent tokens should be 1.5M, got \(run.parentTotalTokens)")
54
+ precondition(run.formattedWorkerTokens == "500.0k", "Formatted worker tokens: \(run.formattedWorkerTokens)")
55
+ precondition(abs(run.workerTokenShare - 0.25) < 0.001, "Worker share should be 25%")
56
+ precondition(run.formattedWorkerSharePercent == "25.0%", "Worker share formatted should be 25.0%")
57
+ }
58
+
59
+ private static func testRunWithZeroWorkers() {
60
+ let parentUsage = TokenUsage(
61
+ totalTokens: 676_000,
62
+ promptTokens: 670_000,
63
+ completionTokens: 6_000
64
+ )
65
+ let parent = ParticipantInfo(
66
+ agentId: "p-2",
67
+ agentType: "parent",
68
+ model: "gpt-5.6-sol",
69
+ usageDelta: parentUsage
70
+ )
71
+
72
+ let run = TaskRun(
73
+ sessionId: "sess-2",
74
+ turnId: "turn-2",
75
+ status: "completed",
76
+ parent: parent,
77
+ workers: []
78
+ )
79
+
80
+ precondition(run.totalTokens == 676_000)
81
+ precondition(run.workerTotalTokens == 0)
82
+ precondition(run.parentTotalTokens == 676_000)
83
+ precondition(run.formattedWorkerTokens == "0")
84
+ precondition(run.workerTokenShare == 0.0)
85
+ precondition(run.formattedWorkerSharePercent == "0.0%")
86
+ }
87
+
88
+ private static func testChatSessionWorkerAggregation() {
89
+ let parentUsage1 = TokenUsage(totalTokens: 8_000_000, promptTokens: 7_900_000, completionTokens: 100_000)
90
+ let workerUsage1 = TokenUsage(totalTokens: 2_000_000, promptTokens: 1_950_000, completionTokens: 50_000)
91
+ let run1 = TaskRun(
92
+ sessionId: "sess-chat",
93
+ turnId: "turn-c1",
94
+ status: "completed",
95
+ parent: ParticipantInfo(agentId: "p1", model: "gpt-5.6-sol", usageDelta: parentUsage1),
96
+ workers: [ParticipantInfo(agentId: "w1", model: "gpt-5.6-luna", usage: workerUsage1)]
97
+ )
98
+
99
+ let parentUsage2 = TokenUsage(totalTokens: 10_000_000, promptTokens: 9_900_000, completionTokens: 100_000)
100
+ let run2 = TaskRun(
101
+ sessionId: "sess-chat",
102
+ turnId: "turn-c2",
103
+ status: "completed",
104
+ parent: ParticipantInfo(agentId: "p2", model: "gpt-5.6-sol", usageDelta: parentUsage2),
105
+ workers: []
106
+ )
107
+
108
+ let chat = ChatSession(
109
+ sessionId: "sess-chat",
110
+ projectName: "codex-flow",
111
+ title: "Test Chat",
112
+ runs: [run2, run1]
113
+ )
114
+
115
+ precondition(chat.totalTokens == 20_000_000, "Chat total tokens should be 20M, got \(chat.totalTokens)")
116
+ precondition(chat.workerTotalTokens == 2_000_000, "Chat worker tokens should be 2M, got \(chat.workerTotalTokens)")
117
+ precondition(chat.parentTotalTokens == 18_000_000, "Chat parent tokens should be 18M, got \(chat.parentTotalTokens)")
118
+ precondition(chat.formattedWorkerTokens == "2.0M", "Chat formatted worker tokens should be 2.0M, got \(chat.formattedWorkerTokens)")
119
+ precondition(abs(chat.workerTokenShare - 0.10) < 0.001, "Chat worker share should be 10%")
120
+ precondition(chat.formattedWorkerSharePercent == "10.0%")
121
+ }
122
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ BIN_DIR="$SCRIPT_DIR/bin"
6
+ OUTPUT="$BIN_DIR/codex-flow-overlay"
7
+ FLOWPILOT_OUTPUT="$BIN_DIR/FlowPilot"
8
+ TMP_OUTPUT="$BIN_DIR/.codex-flow-overlay.$$"
9
+ TMP_FLOWPILOT="$BIN_DIR/.FlowPilot.$$"
10
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
11
+ state_tmp_flowpilot=""
12
+ state_tmp_overlay=""
13
+
14
+ cleanup() {
15
+ rm -f "$TMP_OUTPUT" "$TMP_FLOWPILOT"
16
+ [[ -z "$state_tmp_flowpilot" ]] || rm -f "$state_tmp_flowpilot"
17
+ [[ -z "$state_tmp_overlay" ]] || rm -f "$state_tmp_overlay"
18
+ }
19
+ trap cleanup EXIT
20
+
21
+ mkdir -p "$BIN_DIR"
22
+
23
+ echo "🔨 Building FlowPilot native macOS widget (this usually takes ~40-50s on macOS)..."
24
+
25
+ SWIFT_FILES=()
26
+ while IFS= read -r -d '' file; do
27
+ SWIFT_FILES+=("$file")
28
+ done < <(find "$SCRIPT_DIR/Sources" -name "*.swift" -print0)
29
+
30
+ CORES="$(sysctl -n hw.ncpu 2>/dev/null || echo 2)"
31
+ if [ "$CORES" -gt 4 ]; then
32
+ CORES=4
33
+ fi
34
+
35
+ # Build to a fresh inode first. Replacing a currently executing macOS binary in
36
+ # place can invalidate mapped executable pages and SIGKILL the running process.
37
+ # Atomic rename keeps the old inode alive for the current process while new
38
+ # launches immediately see the freshly built binary.
39
+ swiftc \
40
+ -num-threads "$CORES" \
41
+ -j "$CORES" \
42
+ -framework Cocoa \
43
+ -framework SwiftUI \
44
+ -framework Combine \
45
+ "${SWIFT_FILES[@]}" \
46
+ -o "$TMP_OUTPUT"
47
+
48
+ chmod +x "$TMP_OUTPUT"
49
+ mv -f "$TMP_OUTPUT" "$OUTPUT"
50
+ cp "$OUTPUT" "$TMP_FLOWPILOT"
51
+ chmod +x "$TMP_FLOWPILOT"
52
+ mv -f "$TMP_FLOWPILOT" "$FLOWPILOT_OUTPUT"
53
+ ln -sf "codex-flow-overlay" "$BIN_DIR/flow-pilot"
54
+
55
+ # Sync to installed state dir if it exists. Use the same atomic replacement so
56
+ # rebuilding from the console never corrupts a currently running installed copy.
57
+ STATE_BIN="$CODEX_HOME/codex-flow/bin"
58
+ if [[ -d "$STATE_BIN" ]]; then
59
+ state_tmp_flowpilot="$STATE_BIN/.FlowPilot.$$"
60
+ state_tmp_overlay="$STATE_BIN/.codex-flow-overlay.$$"
61
+ cp "$FLOWPILOT_OUTPUT" "$state_tmp_flowpilot"
62
+ cp "$OUTPUT" "$state_tmp_overlay"
63
+ chmod +x "$state_tmp_flowpilot" "$state_tmp_overlay"
64
+ mv -f "$state_tmp_flowpilot" "$STATE_BIN/FlowPilot"
65
+ state_tmp_flowpilot=""
66
+ mv -f "$state_tmp_overlay" "$STATE_BIN/codex-flow-overlay"
67
+ state_tmp_overlay=""
68
+ fi
69
+
70
+ echo "✨ Build succeeded: $FLOWPILOT_OUTPUT"
71
+ echo ""
72
+ echo "🚀 Quick Start:"
73
+ echo " • 启动悬浮窗: codex-flow overlay start"
74
+ echo " • 或独立运行: $FLOWPILOT_OUTPUT start &"
75
+ echo ""