@miphamai/cli 0.39.1 → 0.40.0
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.
- package/bin/mipham.ts +4 -13
- package/package.json +3 -4
- package/src/core/auto-memory.ts +23 -5
- package/src/core/engine.ts +5 -0
- package/src/core/metrics.ts +148 -72
- package/src/daemon/index.ts +2 -1
- package/src/daemon/logger.ts +84 -0
- package/src/daemon/schedule-manager.ts +2 -1
- package/src/daemon/server.ts +6 -4
- package/src/daemon/worker-pool.ts +2 -1
- package/src/index.tsx +10 -1
- package/src/providers/registry.ts +19 -1
- package/src/shared/package-info.ts +4 -11
- package/src/ui/commands.ts +51 -7
package/bin/mipham.ts
CHANGED
|
@@ -1094,17 +1094,9 @@ async function main() {
|
|
|
1094
1094
|
process.on('SIGINT', restoreTerminal)
|
|
1095
1095
|
process.on('SIGTERM', restoreTerminal)
|
|
1096
1096
|
|
|
1097
|
-
// ──
|
|
1098
|
-
const {
|
|
1099
|
-
const
|
|
1100
|
-
let APP_VERSION = '0.0.0'
|
|
1101
|
-
try {
|
|
1102
|
-
const pkgPath = join(import.meta.dirname!, '..', 'package.json')
|
|
1103
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
1104
|
-
APP_VERSION = pkg.version
|
|
1105
|
-
} catch {
|
|
1106
|
-
// fallback — version will be '0.0.0'
|
|
1107
|
-
}
|
|
1097
|
+
// ── Version: build-time constant (bundled into the compiled binary) ──
|
|
1098
|
+
const { PACKAGE_NAME, PACKAGE_VERSION } = await import('../src/shared/package-info')
|
|
1099
|
+
const APP_VERSION = PACKAGE_VERSION
|
|
1108
1100
|
|
|
1109
1101
|
// ── Version flag ──────────────────────────────────────────────────────────
|
|
1110
1102
|
if (
|
|
@@ -1112,8 +1104,7 @@ async function main() {
|
|
|
1112
1104
|
process.argv.includes('-v') ||
|
|
1113
1105
|
process.argv.includes('-V')
|
|
1114
1106
|
) {
|
|
1115
|
-
|
|
1116
|
-
console.log(`${pkg.name} v${pkg.version}`)
|
|
1107
|
+
console.log(`${PACKAGE_NAME} v${PACKAGE_VERSION}`)
|
|
1117
1108
|
process.exit(0)
|
|
1118
1109
|
}
|
|
1119
1110
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -29,15 +29,14 @@
|
|
|
29
29
|
"bin/",
|
|
30
30
|
"src/",
|
|
31
31
|
"skills/",
|
|
32
|
-
"assets/"
|
|
33
|
-
"dist/"
|
|
32
|
+
"assets/"
|
|
34
33
|
],
|
|
35
34
|
"engines": {
|
|
36
35
|
"node": ">=22.0.0"
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
38
|
"dev": "bun run bin/mipham.ts",
|
|
40
|
-
"build": "bun build --compile --minify
|
|
39
|
+
"build": "bun build --compile --minify ./bin/mipham.ts --outfile dist/mipham",
|
|
41
40
|
"typecheck": "tsc --noEmit",
|
|
42
41
|
"test": "vitest run"
|
|
43
42
|
},
|
package/src/core/auto-memory.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { ExperienceRuleEngine, ToolRule } from './rule-engine.js'
|
|
|
17
17
|
import type { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
|
|
18
18
|
import type { ErrorSignatureDB } from './error-signature-db.js'
|
|
19
19
|
import type { CrsiProvenanceBridge } from '../agent/crsi-provenance-bridge.js'
|
|
20
|
+
import { getMetrics } from './metrics'
|
|
20
21
|
import { join } from 'node:path'
|
|
21
22
|
import { homedir } from 'node:os'
|
|
22
23
|
|
|
@@ -228,11 +229,28 @@ export class AutoMemoryEngine {
|
|
|
228
229
|
this.writeSessionSummary()
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
// Flush CRSI
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
// Flush CRSI effectiveness (evaluate + apply auto-management).
|
|
233
|
+
this.flushEffectiveness()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Evaluate rule effectiveness and apply auto-management to the rule engine:
|
|
238
|
+
* ineffective rules are disabled so intercept() stops injecting them, and
|
|
239
|
+
* recovered rules are re-enabled. Without this, `evaluate()`'s verdicts are
|
|
240
|
+
* computed but never acted upon — the self-improvement loop stays open.
|
|
241
|
+
*/
|
|
242
|
+
flushEffectiveness(): void {
|
|
243
|
+
if (!this.effectivenessTracker) return
|
|
244
|
+
|
|
245
|
+
const { upgrades, disables } = this.effectivenessTracker.evaluate()
|
|
246
|
+
this.effectivenessTracker.persist()
|
|
247
|
+
|
|
248
|
+
if (this.ruleEngine) {
|
|
249
|
+
for (const id of disables) {
|
|
250
|
+
this.ruleEngine.setRuleEnabled(id, false)
|
|
251
|
+
getMetrics().crsiRuleDisables.inc()
|
|
252
|
+
}
|
|
253
|
+
for (const id of upgrades) this.ruleEngine.setRuleEnabled(id, true)
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
|
package/src/core/engine.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { DreamEngine } from './dream-engine.js'
|
|
|
35
35
|
import { ConstitutionLoader } from './constitution-loader.js'
|
|
36
36
|
import { SelfCritique } from './self-critique.js'
|
|
37
37
|
import { UsageTracker } from './usage-tracker'
|
|
38
|
+
import { getMetrics } from './metrics'
|
|
38
39
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
39
40
|
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
40
41
|
import { getMessageBus, type AgentMessage } from '../agent/message-bus'
|
|
@@ -921,6 +922,7 @@ export class QueryEngine {
|
|
|
921
922
|
}
|
|
922
923
|
|
|
923
924
|
private async executeTool(name: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
925
|
+
getMetrics().toolCalls.inc({ tool_name: name })
|
|
924
926
|
const tool = this.tools.get(name)
|
|
925
927
|
if (!tool) {
|
|
926
928
|
// P2-4: Provide a more helpful error for unavailable tools
|
|
@@ -970,6 +972,7 @@ export class QueryEngine {
|
|
|
970
972
|
if (this.crsiConfig?.preToolHook !== false && this.ruleEngine) {
|
|
971
973
|
const ruleResult = this.ruleEngine.intercept(name, effectiveParams)
|
|
972
974
|
if (Object.keys(ruleResult.modified).length > 0) {
|
|
975
|
+
getMetrics().crsiRuleApplications.inc()
|
|
973
976
|
effectiveParams = ruleResult.modified
|
|
974
977
|
}
|
|
975
978
|
if (ruleResult.warnings.length > 0) {
|
|
@@ -980,6 +983,7 @@ export class QueryEngine {
|
|
|
980
983
|
// ── SIS PreFlightChecker — known error pattern interception ──
|
|
981
984
|
const preflight = this.getPreFlightChecker().check(name, effectiveParams)
|
|
982
985
|
if (preflight.action === 'block') {
|
|
986
|
+
getMetrics().sisInterceptions.inc()
|
|
983
987
|
return {
|
|
984
988
|
success: false,
|
|
985
989
|
content: preflight.warning || 'SIS 免疫系统拦截了此操作',
|
|
@@ -987,6 +991,7 @@ export class QueryEngine {
|
|
|
987
991
|
}
|
|
988
992
|
}
|
|
989
993
|
if (preflight.action === 'fix' && preflight.modifiedParams) {
|
|
994
|
+
getMetrics().sisInterceptions.inc()
|
|
990
995
|
effectiveParams = preflight.modifiedParams
|
|
991
996
|
if (preflight.warning) {
|
|
992
997
|
hookWarnings = [...hookWarnings, preflight.warning]
|
package/src/core/metrics.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* collected in-memory and can be exported in Prometheus text format
|
|
7
7
|
* or JSON.
|
|
8
8
|
*
|
|
9
|
+
* All metric types support *labels*: calling `.inc({ tool_name: 'bash' })`
|
|
10
|
+
* records a value for the `tool_name="bash"` series, distinct from the
|
|
11
|
+
* unlabelled series. The registry deduplicates metric families by
|
|
12
|
+
* `name + base labels`, so a family is created once and reused.
|
|
13
|
+
*
|
|
9
14
|
* Usage:
|
|
10
15
|
* import { getMetrics } from '../core/metrics.js'
|
|
11
16
|
*
|
|
@@ -25,7 +30,7 @@ export interface MetricLabels {
|
|
|
25
30
|
[key: string]: string
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
/** Format labels into Prometheus {...} string. */
|
|
33
|
+
/** Format labels into Prometheus {...} string (keys sorted for determinism). */
|
|
29
34
|
function formatLabels(labels?: MetricLabels): string {
|
|
30
35
|
if (!labels || Object.keys(labels).length === 0) return ''
|
|
31
36
|
const parts = Object.entries(labels)
|
|
@@ -34,36 +39,53 @@ function formatLabels(labels?: MetricLabels): string {
|
|
|
34
39
|
return `{${parts.join(',')}}`
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/** Merge base labels with an extra set and format the result. */
|
|
43
|
+
function mergedLabels(base: MetricLabels, extra?: MetricLabels): string {
|
|
44
|
+
return formatLabels({ ...base, ...(extra ?? {}) })
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
// ── Counter ──────────────────────────────────────────────────────────────
|
|
38
48
|
|
|
39
49
|
export class Counter {
|
|
40
50
|
readonly name: string
|
|
41
51
|
readonly help: string
|
|
42
|
-
readonly
|
|
43
|
-
|
|
52
|
+
readonly baseLabels: MetricLabels
|
|
53
|
+
/** key = formatted merged labels, value = running count. */
|
|
54
|
+
private _series = new Map<string, number>()
|
|
44
55
|
|
|
45
56
|
constructor(name: string, help: string, labels?: MetricLabels) {
|
|
46
57
|
this.name = name
|
|
47
58
|
this.help = help
|
|
48
|
-
this.
|
|
59
|
+
this.baseLabels = labels ?? {}
|
|
60
|
+
// Seed the base series so the metric is always present (even at 0).
|
|
61
|
+
this._series.set(formatLabels(this.baseLabels), 0)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
/** Increment. Accepts either an amount, labels, or both via a second arg. */
|
|
65
|
+
inc(amountOrLabels?: number | MetricLabels, maybeAmount?: number): void {
|
|
66
|
+
let amount = 1
|
|
67
|
+
let extra: MetricLabels | undefined
|
|
68
|
+
if (typeof amountOrLabels === 'number') amount = amountOrLabels
|
|
69
|
+
else if (amountOrLabels) extra = amountOrLabels
|
|
70
|
+
if (maybeAmount !== undefined) amount = maybeAmount
|
|
71
|
+
const key = mergedLabels(this.baseLabels, extra)
|
|
72
|
+
this._series.set(key, (this._series.get(key) ?? 0) + amount)
|
|
53
73
|
}
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/** Current value of a labelled series (base series when no labels given). */
|
|
76
|
+
value(extra?: MetricLabels): number {
|
|
77
|
+
return this._series.get(mergedLabels(this.baseLabels, extra)) ?? 0
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
/**
|
|
80
|
+
/** Identity key for registry dedup (name + base labels). */
|
|
60
81
|
get key(): string {
|
|
61
|
-
return this.name + formatLabels(this.
|
|
82
|
+
return this.name + formatLabels(this.baseLabels)
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
toPrometheus(): string {
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
return Array.from(this._series.entries())
|
|
87
|
+
.map(([labelStr, v]) => `${this.name}${labelStr} ${v}`)
|
|
88
|
+
.join('\n')
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
toJSON(): object {
|
|
@@ -71,8 +93,7 @@ export class Counter {
|
|
|
71
93
|
name: this.name,
|
|
72
94
|
type: 'counter',
|
|
73
95
|
help: this.help,
|
|
74
|
-
|
|
75
|
-
value: this._value,
|
|
96
|
+
series: Array.from(this._series.entries()).map(([labels, value]) => ({ labels, value })),
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
}
|
|
@@ -82,38 +103,52 @@ export class Counter {
|
|
|
82
103
|
export class Gauge {
|
|
83
104
|
readonly name: string
|
|
84
105
|
readonly help: string
|
|
85
|
-
readonly
|
|
86
|
-
private
|
|
106
|
+
readonly baseLabels: MetricLabels
|
|
107
|
+
private _series = new Map<string, number>()
|
|
87
108
|
|
|
88
109
|
constructor(name: string, help: string, labels?: MetricLabels) {
|
|
89
110
|
this.name = name
|
|
90
111
|
this.help = help
|
|
91
|
-
this.
|
|
112
|
+
this.baseLabels = labels ?? {}
|
|
113
|
+
this._series.set(formatLabels(this.baseLabels), 0)
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
inc(
|
|
95
|
-
|
|
116
|
+
inc(amountOrLabels?: number | MetricLabels, maybeAmount?: number): void {
|
|
117
|
+
let amount = 1
|
|
118
|
+
let extra: MetricLabels | undefined
|
|
119
|
+
if (typeof amountOrLabels === 'number') amount = amountOrLabels
|
|
120
|
+
else if (amountOrLabels) extra = amountOrLabels
|
|
121
|
+
if (maybeAmount !== undefined) amount = maybeAmount
|
|
122
|
+
const key = mergedLabels(this.baseLabels, extra)
|
|
123
|
+
this._series.set(key, (this._series.get(key) ?? 0) + amount)
|
|
96
124
|
}
|
|
97
125
|
|
|
98
|
-
dec(
|
|
99
|
-
|
|
126
|
+
dec(amountOrLabels?: number | MetricLabels, maybeAmount?: number): void {
|
|
127
|
+
let amount = 1
|
|
128
|
+
let extra: MetricLabels | undefined
|
|
129
|
+
if (typeof amountOrLabels === 'number') amount = amountOrLabels
|
|
130
|
+
else if (amountOrLabels) extra = amountOrLabels
|
|
131
|
+
if (maybeAmount !== undefined) amount = maybeAmount
|
|
132
|
+
const key = mergedLabels(this.baseLabels, extra)
|
|
133
|
+
this._series.set(key, (this._series.get(key) ?? 0) - amount)
|
|
100
134
|
}
|
|
101
135
|
|
|
102
|
-
set(value: number): void {
|
|
103
|
-
this.
|
|
136
|
+
set(value: number, extra?: MetricLabels): void {
|
|
137
|
+
this._series.set(mergedLabels(this.baseLabels, extra), value)
|
|
104
138
|
}
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
return this.
|
|
140
|
+
value(extra?: MetricLabels): number {
|
|
141
|
+
return this._series.get(mergedLabels(this.baseLabels, extra)) ?? 0
|
|
108
142
|
}
|
|
109
143
|
|
|
110
144
|
get key(): string {
|
|
111
|
-
return this.name + formatLabels(this.
|
|
145
|
+
return this.name + formatLabels(this.baseLabels)
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
toPrometheus(): string {
|
|
115
|
-
|
|
116
|
-
|
|
149
|
+
return Array.from(this._series.entries())
|
|
150
|
+
.map(([labelStr, v]) => `${this.name}${labelStr} ${v}`)
|
|
151
|
+
.join('\n')
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
toJSON(): object {
|
|
@@ -121,8 +156,7 @@ export class Gauge {
|
|
|
121
156
|
name: this.name,
|
|
122
157
|
type: 'gauge',
|
|
123
158
|
help: this.help,
|
|
124
|
-
|
|
125
|
-
value: this._value,
|
|
159
|
+
series: Array.from(this._series.entries()).map(([labels, value]) => ({ labels, value })),
|
|
126
160
|
}
|
|
127
161
|
}
|
|
128
162
|
}
|
|
@@ -131,82 +165,100 @@ export class Gauge {
|
|
|
131
165
|
|
|
132
166
|
const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
133
167
|
|
|
168
|
+
interface HistogramSeries {
|
|
169
|
+
labels: MetricLabels
|
|
170
|
+
count: number
|
|
171
|
+
sum: number
|
|
172
|
+
bucketCounts: number[]
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
export class Histogram {
|
|
135
176
|
readonly name: string
|
|
136
177
|
readonly help: string
|
|
137
|
-
readonly
|
|
178
|
+
readonly baseLabels: MetricLabels
|
|
138
179
|
readonly buckets: number[]
|
|
139
|
-
private
|
|
140
|
-
private _sum = 0
|
|
141
|
-
private _bucketCounts: number[]
|
|
180
|
+
private _series = new Map<string, HistogramSeries>()
|
|
142
181
|
|
|
143
182
|
constructor(name: string, help: string, buckets?: number[], labels?: MetricLabels) {
|
|
144
183
|
this.name = name
|
|
145
184
|
this.help = help
|
|
146
|
-
this.labels = labels ?? {}
|
|
147
185
|
this.buckets = buckets ?? DEFAULT_BUCKETS
|
|
148
|
-
this.
|
|
186
|
+
this.baseLabels = labels ?? {}
|
|
187
|
+
this._series.set(formatLabels(this.baseLabels), {
|
|
188
|
+
labels: this.baseLabels,
|
|
189
|
+
count: 0,
|
|
190
|
+
sum: 0,
|
|
191
|
+
bucketCounts: new Array(this.buckets.length).fill(0),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private getSeries(extra?: MetricLabels): HistogramSeries {
|
|
196
|
+
const key = mergedLabels(this.baseLabels, extra)
|
|
197
|
+
let s = this._series.get(key)
|
|
198
|
+
if (!s) {
|
|
199
|
+
s = {
|
|
200
|
+
labels: { ...this.baseLabels, ...(extra ?? {}) },
|
|
201
|
+
count: 0,
|
|
202
|
+
sum: 0,
|
|
203
|
+
bucketCounts: new Array(this.buckets.length).fill(0),
|
|
204
|
+
}
|
|
205
|
+
this._series.set(key, s)
|
|
206
|
+
}
|
|
207
|
+
return s
|
|
149
208
|
}
|
|
150
209
|
|
|
151
|
-
observe(value: number): void {
|
|
152
|
-
this.
|
|
153
|
-
|
|
210
|
+
observe(value: number, extra?: MetricLabels): void {
|
|
211
|
+
const s = this.getSeries(extra)
|
|
212
|
+
s.count++
|
|
213
|
+
s.sum += value
|
|
154
214
|
for (let i = 0; i < this.buckets.length; i++) {
|
|
155
215
|
if (value <= (this.buckets[i] ?? Infinity)) {
|
|
156
|
-
|
|
216
|
+
s.bucketCounts[i] = (s.bucketCounts[i] ?? 0) + 1
|
|
157
217
|
}
|
|
158
218
|
}
|
|
159
219
|
}
|
|
160
220
|
|
|
161
|
-
get
|
|
162
|
-
return this.
|
|
221
|
+
get key(): string {
|
|
222
|
+
return this.name + formatLabels(this.baseLabels)
|
|
163
223
|
}
|
|
164
224
|
|
|
165
|
-
|
|
166
|
-
|
|
225
|
+
/** Total observations in the base (unlabelled) series. */
|
|
226
|
+
get count(): number {
|
|
227
|
+
return this._series.get(formatLabels(this.baseLabels))?.count ?? 0
|
|
167
228
|
}
|
|
168
229
|
|
|
169
|
-
|
|
170
|
-
|
|
230
|
+
/** Sum of observations in the base (unlabelled) series. */
|
|
231
|
+
get sum(): number {
|
|
232
|
+
return this._series.get(formatLabels(this.baseLabels))?.sum ?? 0
|
|
171
233
|
}
|
|
172
234
|
|
|
173
235
|
toPrometheus(): string {
|
|
174
|
-
const labelStr = formatLabels(this.labels)
|
|
175
236
|
const lines: string[] = []
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
})
|
|
183
|
-
|
|
237
|
+
for (const s of this._series.values()) {
|
|
238
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
239
|
+
const leLabel = formatLabels({ ...s.labels, le: String(this.buckets[i]) })
|
|
240
|
+
lines.push(`${this.name}_bucket${leLabel} ${s.bucketCounts[i] ?? 0}`)
|
|
241
|
+
}
|
|
242
|
+
const infLabel = formatLabels({ ...s.labels, le: '+Inf' })
|
|
243
|
+
lines.push(`${this.name}_bucket${infLabel} ${s.count}`)
|
|
244
|
+
const labelStr = formatLabels(s.labels)
|
|
245
|
+
lines.push(`${this.name}_sum${labelStr} ${s.sum}`)
|
|
246
|
+
lines.push(`${this.name}_count${labelStr} ${s.count}`)
|
|
184
247
|
}
|
|
185
|
-
// +Inf bucket
|
|
186
|
-
const infLabel = formatLabels({ ...this.labels, le: '+Inf' })
|
|
187
|
-
lines.push(`${this.name}_bucket${infLabel} ${this._count}`)
|
|
188
|
-
|
|
189
|
-
// _sum and _count
|
|
190
|
-
lines.push(`${this.name}_sum${labelStr} ${this._sum}`)
|
|
191
|
-
lines.push(`${this.name}_count${labelStr} ${this._count}`)
|
|
192
|
-
|
|
193
248
|
return lines.join('\n')
|
|
194
249
|
}
|
|
195
250
|
|
|
196
251
|
toJSON(): object {
|
|
197
|
-
const bucketResults: { le: string; count: number }[] = []
|
|
198
|
-
for (let i = 0; i < this.buckets.length; i++) {
|
|
199
|
-
bucketResults.push({ le: String(this.buckets[i]), count: this._bucketCounts[i] ?? 0 })
|
|
200
|
-
}
|
|
201
|
-
bucketResults.push({ le: '+Inf', count: this._count })
|
|
202
252
|
return {
|
|
203
253
|
name: this.name,
|
|
204
254
|
type: 'histogram',
|
|
205
255
|
help: this.help,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
256
|
+
series: Array.from(this._series.values()).map((s) => ({
|
|
257
|
+
labels: s.labels,
|
|
258
|
+
count: s.count,
|
|
259
|
+
sum: s.sum,
|
|
260
|
+
buckets: this.buckets.map((le, i) => ({ le: String(le), count: s.bucketCounts[i] ?? 0 })),
|
|
261
|
+
})),
|
|
210
262
|
}
|
|
211
263
|
}
|
|
212
264
|
}
|
|
@@ -238,6 +290,15 @@ export class MetricsRegistry {
|
|
|
238
290
|
/** Active CLI sessions gauge. */
|
|
239
291
|
readonly activeSessions: Gauge
|
|
240
292
|
|
|
293
|
+
/** CRSI auto-fix rule applications counter. */
|
|
294
|
+
readonly crsiRuleApplications: Counter
|
|
295
|
+
|
|
296
|
+
/** CRSI rules disabled for low effectiveness counter. */
|
|
297
|
+
readonly crsiRuleDisables: Counter
|
|
298
|
+
|
|
299
|
+
/** SIS immune system interceptions (block/fix of known error patterns). */
|
|
300
|
+
readonly sisInterceptions: Counter
|
|
301
|
+
|
|
241
302
|
constructor() {
|
|
242
303
|
// Pre-register standard metrics
|
|
243
304
|
this.cliInvocations = this.counter(
|
|
@@ -264,6 +325,21 @@ export class MetricsRegistry {
|
|
|
264
325
|
)
|
|
265
326
|
|
|
266
327
|
this.activeSessions = this.gauge('mipham_code_active_sessions', 'Number of active CLI sessions')
|
|
328
|
+
|
|
329
|
+
this.crsiRuleApplications = this.counter(
|
|
330
|
+
'mipham_code_crsi_rule_applications_total',
|
|
331
|
+
'Number of CRSI auto-fix rule applications',
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
this.crsiRuleDisables = this.counter(
|
|
335
|
+
'mipham_code_crsi_rule_disables_total',
|
|
336
|
+
'Number of CRSI rules disabled for low effectiveness',
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
this.sisInterceptions = this.counter(
|
|
340
|
+
'mipham_code_sis_interceptions_total',
|
|
341
|
+
'Number of SIS immune system interceptions',
|
|
342
|
+
)
|
|
267
343
|
}
|
|
268
344
|
|
|
269
345
|
// ── Factory methods ─────────────────────────────────────────────
|
package/src/daemon/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { loadOrCreateToken } from './auth'
|
|
|
22
22
|
import { RateLimiter } from './rate-limiter'
|
|
23
23
|
import { PACKAGE_VERSION } from '../shared/package-info'
|
|
24
24
|
import type { DaemonStatus } from './types'
|
|
25
|
+
import { logger } from './logger'
|
|
25
26
|
|
|
26
27
|
const HOME = homedir()
|
|
27
28
|
const MIPHAM_HOME = join(HOME, '.mipham')
|
|
@@ -133,7 +134,7 @@ export async function startDaemon(): Promise<{ port: number; token: string }> {
|
|
|
133
134
|
if (sessionsCount === 0) {
|
|
134
135
|
const migrated = db.migrateFromJsonl()
|
|
135
136
|
if (migrated > 0) {
|
|
136
|
-
|
|
137
|
+
logger.info('migrated JSONL sessions to SQLite', { count: migrated })
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structured JSON logger for the Mipham Code daemon.
|
|
3
|
+
*
|
|
4
|
+
* Emits one JSON object per line to stdout (debug/info) or stderr
|
|
5
|
+
* (warn/error), so logs can be aggregated by the process manager
|
|
6
|
+
* (PM2, systemd, Docker) without ad-hoc string formatting.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { logger } from './logger'
|
|
10
|
+
* logger.info('migrated sessions', { count })
|
|
11
|
+
* logger.child({ sessionId }).error('prompt failed', { error: err })
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
15
|
+
|
|
16
|
+
export interface LogFields {
|
|
17
|
+
[key: string]: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 }
|
|
21
|
+
|
|
22
|
+
/** Serialize a value for JSON logging, converting Error to {message, stack}. */
|
|
23
|
+
function sanitizeValue(v: unknown): unknown {
|
|
24
|
+
if (v instanceof Error) {
|
|
25
|
+
return { message: v.message, stack: v.stack }
|
|
26
|
+
}
|
|
27
|
+
return v
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Logger {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly name: string,
|
|
33
|
+
private readonly fields: LogFields = {},
|
|
34
|
+
private readonly minLevel: LogLevel = 'info',
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
/** Return a child logger with extra context fields merged in. */
|
|
38
|
+
child(fields: LogFields): Logger {
|
|
39
|
+
return new Logger(this.name, { ...this.fields, ...fields }, this.minLevel)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private emit(level: LogLevel, msg: string, fields?: LogFields): void {
|
|
43
|
+
if (ORDER[level] < ORDER[this.minLevel]) return
|
|
44
|
+
|
|
45
|
+
const entry: Record<string, unknown> = {
|
|
46
|
+
ts: new Date().toISOString(),
|
|
47
|
+
level,
|
|
48
|
+
name: this.name,
|
|
49
|
+
msg,
|
|
50
|
+
...this.fields,
|
|
51
|
+
}
|
|
52
|
+
if (fields) {
|
|
53
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
54
|
+
entry[k] = sanitizeValue(v)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const line = JSON.stringify(entry) + '\n'
|
|
59
|
+
if (level === 'warn' || level === 'error') {
|
|
60
|
+
process.stderr.write(line)
|
|
61
|
+
} else {
|
|
62
|
+
process.stdout.write(line)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
debug(msg: string, fields?: LogFields): void {
|
|
67
|
+
this.emit('debug', msg, fields)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
info(msg: string, fields?: LogFields): void {
|
|
71
|
+
this.emit('info', msg, fields)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
warn(msg: string, fields?: LogFields): void {
|
|
75
|
+
this.emit('warn', msg, fields)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
error(msg: string, fields?: LogFields): void {
|
|
79
|
+
this.emit('error', msg, fields)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Shared daemon logger. */
|
|
84
|
+
export const logger = new Logger('mipham-daemon')
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { DaemonDatabase } from './database'
|
|
6
6
|
import type { WorkerPool } from './worker-pool'
|
|
7
7
|
import type { DaemonSchedule } from './types'
|
|
8
|
+
import { logger } from './logger'
|
|
8
9
|
|
|
9
10
|
// Simple minute-granularity cron expression parser.
|
|
10
11
|
//
|
|
@@ -125,7 +126,7 @@ export class ScheduleManager {
|
|
|
125
126
|
if (!worker) continue
|
|
126
127
|
|
|
127
128
|
worker.processPrompt(schedule.prompt).catch((err: unknown) => {
|
|
128
|
-
|
|
129
|
+
logger.error('error firing schedule', { scheduleId: schedule.id, error: err })
|
|
129
130
|
})
|
|
130
131
|
|
|
131
132
|
const now = new Date()
|
package/src/daemon/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { RateLimiter } from './rate-limiter'
|
|
|
12
12
|
import { corsMiddleware, addCorsHeaders } from './cors'
|
|
13
13
|
import { PACKAGE_VERSION } from '../shared/package-info'
|
|
14
14
|
import { WorkerPool } from './worker-pool'
|
|
15
|
+
import { logger } from './logger'
|
|
15
16
|
import type { SessionWorker } from './session-worker'
|
|
16
17
|
import type { ClientMessage } from './attach-protocol'
|
|
17
18
|
import { QueryEngine } from '../core/engine'
|
|
@@ -192,7 +193,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
192
193
|
|
|
193
194
|
return worker
|
|
194
195
|
} catch (err) {
|
|
195
|
-
|
|
196
|
+
logger.error('error creating worker', { sessionId, error: err })
|
|
196
197
|
return null
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -208,6 +209,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
208
209
|
const url = new URL(req.url)
|
|
209
210
|
const path = url.pathname
|
|
210
211
|
const method = req.method
|
|
212
|
+
logger.info('request', { method, path })
|
|
211
213
|
|
|
212
214
|
// ── Rate limiting (skip health endpoint) ──────────
|
|
213
215
|
if (path !== '/api/v1/health') {
|
|
@@ -296,7 +298,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
296
298
|
const sessionId = sessionMatch[1]!
|
|
297
299
|
// Stop worker (interrupt, persist, mark idle) before closing session
|
|
298
300
|
pool.stopWorker(sessionId).catch((err: unknown) => {
|
|
299
|
-
|
|
301
|
+
logger.error('error stopping worker', { sessionId, error: err })
|
|
300
302
|
})
|
|
301
303
|
// Remove engine from cache
|
|
302
304
|
engineCache.delete(sessionId)
|
|
@@ -346,7 +348,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
346
348
|
// Fire and forget — respond immediately while processing
|
|
347
349
|
// streams results to connected WebSocket clients.
|
|
348
350
|
worker.processPrompt(prompt).catch((err: unknown) => {
|
|
349
|
-
|
|
351
|
+
logger.error('error processing prompt', { sessionId, error: err })
|
|
350
352
|
})
|
|
351
353
|
|
|
352
354
|
return json({ ok: true, data: { sessionId, status: 'processing' } }, { status: 202 })
|
|
@@ -628,7 +630,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
628
630
|
if (!worker) return
|
|
629
631
|
|
|
630
632
|
worker.processPrompt(parsed.prompt).catch((err: unknown) => {
|
|
631
|
-
|
|
633
|
+
logger.error('error processing WS prompt', { sessionId, error: err })
|
|
632
634
|
})
|
|
633
635
|
break
|
|
634
636
|
}
|
|
@@ -22,6 +22,7 @@ import type { DaemonDatabase } from './database'
|
|
|
22
22
|
import type { QueryEngine } from '../core/engine'
|
|
23
23
|
import type { ContextManager } from '../core/context'
|
|
24
24
|
import type { ProviderRegistry } from '../providers/registry'
|
|
25
|
+
import { logger } from './logger'
|
|
25
26
|
|
|
26
27
|
export class WorkerPool {
|
|
27
28
|
private workers: Map<string, SessionWorker>
|
|
@@ -149,7 +150,7 @@ export class WorkerPool {
|
|
|
149
150
|
this.stopWorker(sessionId).catch((err) => {
|
|
150
151
|
// Log but never crash — this runs in a Timer callback outside
|
|
151
152
|
// any request context
|
|
152
|
-
|
|
153
|
+
logger.error('error auto-stopping idle worker', { sessionId, error: err })
|
|
153
154
|
})
|
|
154
155
|
}, this.idleTimeoutMs)
|
|
155
156
|
|
package/src/index.tsx
CHANGED
|
@@ -193,8 +193,9 @@ function SetupGate(props: SetupGateProps) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
196
|
-
// Metrics: count CLI invocation
|
|
196
|
+
// Metrics: count CLI invocation + active session
|
|
197
197
|
getMetrics().cliInvocations.inc()
|
|
198
|
+
getMetrics().activeSessions.inc()
|
|
198
199
|
|
|
199
200
|
// ── Workspace Trust Check ──
|
|
200
201
|
await checkWorkspaceTrust()
|
|
@@ -591,6 +592,14 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
591
592
|
})
|
|
592
593
|
}
|
|
593
594
|
}
|
|
595
|
+
// Flush CRSI effectiveness — evaluate rules and apply auto-degrade/disable.
|
|
596
|
+
// Best-effort: the self-improvement flush must never block session exit.
|
|
597
|
+
try {
|
|
598
|
+
engine.getAutoMemory().flushEffectiveness()
|
|
599
|
+
} catch {
|
|
600
|
+
// ignore — CRSI flush is non-critical
|
|
601
|
+
}
|
|
602
|
+
getMetrics().activeSessions.dec()
|
|
594
603
|
process.exit(0)
|
|
595
604
|
}
|
|
596
605
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
|
|
2
2
|
import type { Llm } from './llm'
|
|
3
|
+
import { getMetrics } from '../core/metrics'
|
|
3
4
|
|
|
4
5
|
export interface ProviderInstance {
|
|
5
6
|
config: ProviderConfig
|
|
@@ -115,6 +116,23 @@ export class ProviderRegistry implements Llm {
|
|
|
115
116
|
|
|
116
117
|
async *chat(req: ChatRequest): AsyncGenerator<StreamChunk> {
|
|
117
118
|
const provider = this.getActive()
|
|
118
|
-
|
|
119
|
+
const providerId = this.activeProviderId
|
|
120
|
+
const modelId = req.model || this.activeModelId
|
|
121
|
+
const metrics = getMetrics()
|
|
122
|
+
const start = Date.now()
|
|
123
|
+
metrics.modelRequests.inc({ provider: providerId, model: modelId })
|
|
124
|
+
try {
|
|
125
|
+
for await (const chunk of provider.chat({ ...req, model: modelId })) {
|
|
126
|
+
if (chunk.type === 'error') {
|
|
127
|
+
metrics.modelRequestErrors.inc({ provider: providerId, error_type: 'api_error' })
|
|
128
|
+
}
|
|
129
|
+
yield chunk
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
metrics.modelRequestErrors.inc({ provider: providerId, error_type: 'exception' })
|
|
133
|
+
throw err
|
|
134
|
+
} finally {
|
|
135
|
+
metrics.modelRequestDurationMs.observe(Date.now() - start, { provider: providerId })
|
|
136
|
+
}
|
|
119
137
|
}
|
|
120
138
|
}
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mipham Code — package metadata (bundled version)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Compile-time constants (bundled into the compiled binary — no runtime
|
|
5
|
+
* package.json read). Version is synced by scripts/bump-version.sh.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync } from 'node:fs'
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
|
|
11
|
-
// resolve package.json relative to this source file
|
|
12
|
-
const PKG_PATH = path.join(import.meta.dirname!, '..', '..', 'package.json')
|
|
13
|
-
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf-8'))
|
|
14
|
-
|
|
15
8
|
/** npm 包全名(含 scope) */
|
|
16
|
-
export const PACKAGE_NAME
|
|
9
|
+
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
17
10
|
|
|
18
11
|
/** 当前发布版本 */
|
|
19
|
-
export const PACKAGE_VERSION
|
|
12
|
+
export const PACKAGE_VERSION = '0.40.0' as const
|
|
20
13
|
|
|
21
14
|
/** npm install 全局安装命令 */
|
|
22
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/ui/commands.ts
CHANGED
|
@@ -668,6 +668,13 @@ const crsiRestoreCmd: CommandHandler = (ctx, args) => {
|
|
|
668
668
|
return { content: `Rule \`${ruleId}\` has been re-enabled.` }
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
/** Render a tiny ASCII sparkline from a series of 0-1 values (success rates). */
|
|
672
|
+
function crsiSparkline(values: number[]): string {
|
|
673
|
+
if (values.length === 0) return '—'
|
|
674
|
+
const chars = '▁▂▃▄▅▆▇█'
|
|
675
|
+
return values.map((v) => chars[Math.min(Math.max(Math.round(v * 7), 0), 7)] ?? '▁').join('')
|
|
676
|
+
}
|
|
677
|
+
|
|
671
678
|
const crsiStatsCmd: CommandHandler = async (ctx) => {
|
|
672
679
|
const engine = ctx.engine.getRuleEngine()
|
|
673
680
|
const tracker = ctx.engine.getEffectivenessTracker()
|
|
@@ -682,21 +689,43 @@ const crsiStatsCmd: CommandHandler = async (ctx) => {
|
|
|
682
689
|
lines.push(`Auto-generated: ${rules.filter((r) => r.source === 'pattern-analyzer').length}`)
|
|
683
690
|
lines.push(`Manual: ${rules.filter((r) => r.source === 'manual').length}`)
|
|
684
691
|
|
|
685
|
-
|
|
692
|
+
const effs = tracker?.allRules ?? []
|
|
693
|
+
if (effs.length > 0) {
|
|
686
694
|
let totalInterceptions = 0
|
|
687
695
|
let totalSuccesses = 0
|
|
688
|
-
for (const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
totalInterceptions += eff.appliedCount
|
|
692
|
-
totalSuccesses += eff.successAfterCount
|
|
693
|
-
}
|
|
696
|
+
for (const eff of effs) {
|
|
697
|
+
totalInterceptions += eff.appliedCount
|
|
698
|
+
totalSuccesses += eff.successAfterCount
|
|
694
699
|
}
|
|
695
700
|
lines.push('')
|
|
696
701
|
lines.push(`Total interceptions: ${totalInterceptions}`)
|
|
697
702
|
lines.push(
|
|
698
703
|
`Success rate after rules: ${totalInterceptions > 0 ? Math.round((totalSuccesses / totalInterceptions) * 100) : 0}%`,
|
|
699
704
|
)
|
|
705
|
+
|
|
706
|
+
lines.push('')
|
|
707
|
+
lines.push('### Rule effectiveness')
|
|
708
|
+
lines.push('| Rule | Status | Applied | Success | Trend |')
|
|
709
|
+
lines.push('|------|--------|---------|---------|-------|')
|
|
710
|
+
const statusIcon: Record<string, string> = { active: '🟢', degrading: '🟡', disabled: '⚫' }
|
|
711
|
+
for (const eff of effs) {
|
|
712
|
+
const icon = statusIcon[eff.status] ?? '⚪'
|
|
713
|
+
const rate =
|
|
714
|
+
eff.appliedCount > 0 ? Math.round((eff.successAfterCount / eff.appliedCount) * 100) : 0
|
|
715
|
+
const trend = crsiSparkline(eff.evaluationHistory.map((h) => 1 - h.failureRate))
|
|
716
|
+
lines.push(
|
|
717
|
+
`| ${eff.ruleId} | ${icon} ${eff.status} | ${eff.appliedCount} | ${rate}% | ${trend} |`,
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const disabled = effs.filter((e) => e.status === 'disabled')
|
|
722
|
+
if (disabled.length > 0) {
|
|
723
|
+
lines.push('')
|
|
724
|
+
lines.push('### Disabled rules (retired)')
|
|
725
|
+
for (const d of disabled) {
|
|
726
|
+
lines.push(`- ${d.ruleId} (failure rate ${Math.round(d.postRuleFailureRate * 100)}%)`)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
700
729
|
}
|
|
701
730
|
|
|
702
731
|
return { content: lines.join('\n') }
|
|
@@ -907,6 +936,21 @@ const sisStatsCmd: CommandHandler = (ctx) => {
|
|
|
907
936
|
)
|
|
908
937
|
lines.push(`平均成功率: ${Math.round(stats.avgSuccessRate * 100)}%`)
|
|
909
938
|
lines.push(`总拦截次数: ${stats.totalInterceptions}`)
|
|
939
|
+
|
|
940
|
+
const active = db.getActive()
|
|
941
|
+
if (active.length > 0) {
|
|
942
|
+
lines.push('')
|
|
943
|
+
lines.push('### 签名明细(按拦截次数排序)')
|
|
944
|
+
lines.push('| 签名 | 状态 | 拦截 | 成功率 |')
|
|
945
|
+
lines.push('|------|------|------|--------|')
|
|
946
|
+
for (const s of active) {
|
|
947
|
+
const icon = s.status === 'active' ? '🟢' : '🟡'
|
|
948
|
+
lines.push(
|
|
949
|
+
`| ${s.id} | ${icon} ${s.status} | ${s.occurrences} | ${Math.round(s.successRate * 100)}% |`,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
910
954
|
return { content: lines.join('\n') }
|
|
911
955
|
}
|
|
912
956
|
|