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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- 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 ""
|