dsh-tacit 0.3.0 → 0.4.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/README.md +4 -4
- package/client/client.js +75 -29
- package/lib/analyze.js +100 -13
- package/lib/index.js +1 -1
- package/lib/schema.js +29 -15
- package/lib/service.js +86 -97
- package/lib/store.js +64 -3
- package/lib/usage.js +82 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,14 +50,14 @@ already configured in the harness (Tacit never reads it).
|
|
|
50
50
|
| | What | Cost |
|
|
51
51
|
| --- | --- | --- |
|
|
52
52
|
| **Zero-click learning** | messy turns and your own corrections are analyzed in the background, with the previous turn as context; automatic analyses are capped per day (30 by default) | $0.001–0.003 each |
|
|
53
|
-
| **Directives that earn their place** | learned directives are injected as a short system-prompt section you can read, edit, toggle or delete; a new one
|
|
53
|
+
| **Directives that earn their place** | learned directives are injected as a short system-prompt section you can read, edit, toggle or delete; a new one goes on trial (one per scope at a time) and is retired if you start correcting the agent more often | free |
|
|
54
54
|
| **✨ Improve** | a composer button that rewrites your current draft using what Tacit has learned, with a before/after preview and 👍/👎 | $0.001–0.002 per click |
|
|
55
|
-
| **Measured, not guessed** | Settings shows your real trend — messy-turn rate and tokens per turn, first 20 turns vs. latest 20 — and Tacit's own spend: every call metered and priced at list price, shown in Settings | free |
|
|
55
|
+
| **Measured, not guessed** | Settings shows your real trend — how often you correct the agent, messy-turn rate and tokens per turn, first 20 turns vs. latest 20 — and Tacit's own spend: every call metered and priced at list price, shown in Settings | free |
|
|
56
56
|
|
|
57
57
|
<p align="center">
|
|
58
|
-
<img alt="Tacit
|
|
58
|
+
<img alt="Settings → Tacit: the Overview card with automatic learning status and the Usage card with spend tiles, a daily spend chart, spend by operation and the run list" src="https://raw.githubusercontent.com/hackernotfound/dsh-tacit/main/docs/assets/tacit-settings.png" width="960">
|
|
59
59
|
</p>
|
|
60
|
-
<p align="center"><sub>Settings → Tacit, captured from a clean local profile with
|
|
60
|
+
<p align="center"><sub>Settings → Tacit with Overview and Usage expanded, captured from a clean local profile seeded with synthetic runs and directives.</sub></p>
|
|
61
61
|
|
|
62
62
|
## How it works
|
|
63
63
|
|
package/client/client.js
CHANGED
|
@@ -50,6 +50,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
50
50
|
'status.autoOn': '自动学习已开启 · 今日 {today}/{budget}',
|
|
51
51
|
'status.autoOff': '自动学习已关闭 — 仅在你点击「分析」时学习。',
|
|
52
52
|
'status.autoHint': '混乱的轮次(重试、工具错误、压缩)以及你纠正智能体的下一条消息会被自动分析,无需点击。',
|
|
53
|
+
'status.trendCorrections': '你纠正智能体:{a} → {b}',
|
|
53
54
|
'status.trendMessy': '混乱轮次:{a} → {b}',
|
|
54
55
|
'status.trendTokens': '每轮 tokens:{a} → {b}',
|
|
55
56
|
'status.trendHint': '(最早 {n} 轮 vs 最近 {n} 轮,真实数据)',
|
|
@@ -80,6 +81,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
80
81
|
'steer.remove': '删除',
|
|
81
82
|
'steer.preview': '实际注入的文本',
|
|
82
83
|
'steer.distilled': '自动提炼',
|
|
84
|
+
'steer.queued': '等待试用',
|
|
83
85
|
'steer.trial': '试用 {n}/{total}',
|
|
84
86
|
'steer.active': '已生效',
|
|
85
87
|
'steer.retired': '已淘汰 · {reason}',
|
|
@@ -185,6 +187,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
185
187
|
'usage.col.time': '时间',
|
|
186
188
|
'usage.col.op': '操作',
|
|
187
189
|
'usage.col.scope': '范围',
|
|
190
|
+
'usage.scopeAll': '全部会话',
|
|
188
191
|
'usage.col.model': '模型',
|
|
189
192
|
'usage.col.status': '状态',
|
|
190
193
|
'usage.col.calls': '计费调用',
|
|
@@ -236,6 +239,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
236
239
|
'privacy.warnHint': '填 0 表示关闭提醒。达到阈值的 80% 时开始提醒,超过阈值后标记为已超出。',
|
|
237
240
|
'privacy.clearUsage': '清除用量记录',
|
|
238
241
|
'privacy.apply': '应用',
|
|
242
|
+
'privacy.applyTo': '{action}:{field}',
|
|
239
243
|
'confirm.reportsTitle': '清除所有分析报告?',
|
|
240
244
|
'confirm.reportsBody': '这会删除 Tacit 为所有会话写下的全部分析报告。指令、风格规则和用量记录会保留。此操作无法撤销。',
|
|
241
245
|
'confirm.usageTitle': '清除用量记录?',
|
|
@@ -280,6 +284,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
280
284
|
'err.busy': '该轮次的分析已在运行中。',
|
|
281
285
|
'err.no-llm': '无法访问模型服务。',
|
|
282
286
|
'err.no-api-key': 'DeepSeek API Key 缺失或无效(请在 设置 → 模型 中配置)。',
|
|
287
|
+
'err.no-credit': '模型服务商余额或配额已耗尽。',
|
|
283
288
|
'err.rate-limited': '请求被限流,请稍后重试。',
|
|
284
289
|
'err.timeout': '模型调用超时,请重试。',
|
|
285
290
|
'err.empty-response': '模型没有返回内容,请重试。',
|
|
@@ -295,6 +300,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
295
300
|
'status.autoOn': 'Auto-learning on · {today}/{budget} today',
|
|
296
301
|
'status.autoOff': 'Auto-learning off — Tacit only learns when you click Analyze.',
|
|
297
302
|
'status.autoHint': 'Messy turns (retries, tool errors, compactions) and turns you correct in your next message are analyzed automatically — no clicks.',
|
|
303
|
+
'status.trendCorrections': 'You corrected the agent: {a} → {b}',
|
|
298
304
|
'status.trendMessy': 'Messy turns: {a} → {b}',
|
|
299
305
|
'status.trendTokens': 'Tokens/turn: {a} → {b}',
|
|
300
306
|
'status.trendHint': '(first {n} turns vs. latest {n}, measured)',
|
|
@@ -325,6 +331,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
325
331
|
'steer.remove': 'Remove',
|
|
326
332
|
'steer.preview': 'Exact text injected',
|
|
327
333
|
'steer.distilled': 'distilled',
|
|
334
|
+
'steer.queued': 'waiting for trial',
|
|
328
335
|
'steer.trial': 'trial {n}/{total}',
|
|
329
336
|
'steer.active': 'active',
|
|
330
337
|
'steer.retired': 'retired · {reason}',
|
|
@@ -430,6 +437,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
430
437
|
'usage.col.time': 'Time',
|
|
431
438
|
'usage.col.op': 'Operation',
|
|
432
439
|
'usage.col.scope': 'Scope',
|
|
440
|
+
'usage.scopeAll': 'all sessions',
|
|
433
441
|
'usage.col.model': 'Model',
|
|
434
442
|
'usage.col.status': 'Status',
|
|
435
443
|
'usage.col.calls': 'Billed calls',
|
|
@@ -481,6 +489,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
481
489
|
'privacy.warnHint': '0 turns the warning off. Tacit warns at 80 % of the amount and marks it exceeded above it.',
|
|
482
490
|
'privacy.clearUsage': 'Clear usage history',
|
|
483
491
|
'privacy.apply': 'Apply',
|
|
492
|
+
'privacy.applyTo': '{action}: {field}',
|
|
484
493
|
'confirm.reportsTitle': 'Clear all analysis reports?',
|
|
485
494
|
'confirm.reportsBody': 'This deletes every analysis report Tacit has written, for every session. Your directives, style rules and usage history are kept. This cannot be undone.',
|
|
486
495
|
'confirm.usageTitle': 'Clear usage history?',
|
|
@@ -525,6 +534,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
525
534
|
'err.busy': 'An analysis for this turn is already running.',
|
|
526
535
|
'err.no-llm': 'The model service is unavailable.',
|
|
527
536
|
'err.no-api-key': 'The DeepSeek API key is missing or invalid (configure it in Settings → Models).',
|
|
537
|
+
'err.no-credit': 'Your provider balance or quota is exhausted.',
|
|
528
538
|
'err.rate-limited': 'The request was rate-limited; try again shortly.',
|
|
529
539
|
'err.timeout': 'The model call timed out; please retry.',
|
|
530
540
|
'err.empty-response': 'The model returned no content; please retry.',
|
|
@@ -655,7 +665,6 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
655
665
|
usageFilters: { range: '30d', type: '', status: '', model: '', workspace: '', sessionId: '', page: 1, pageSize: 20 },
|
|
656
666
|
usageRuns: {}, // runId → the full run (attempts included), fetched on first expand
|
|
657
667
|
usageExpanded: new Set(), // runIds whose attempt rows are open
|
|
658
|
-
usageLoading: false,
|
|
659
668
|
usageSeries: '30', // '7' | '30' — which sparkline the strip shows
|
|
660
669
|
pricingRefreshing: false, // a /pricing-refresh call is in flight
|
|
661
670
|
initStarted: false,
|
|
@@ -775,14 +784,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
775
784
|
|
|
776
785
|
/** Read the whole cost panel in one call; a failure keeps the last envelope. */
|
|
777
786
|
async function fetchUsage() {
|
|
778
|
-
rootStore.usageLoading = true
|
|
779
787
|
try {
|
|
780
788
|
const result = await api('/usage', usageQuery())
|
|
781
789
|
if (result !== null && typeof result === 'object' && result.ok === true) rootStore.usage = result
|
|
782
790
|
} catch {
|
|
783
791
|
// A stale panel beats a blank one; the next poll tries again.
|
|
784
792
|
}
|
|
785
|
-
rootStore.usageLoading = false
|
|
786
793
|
notifyRoot()
|
|
787
794
|
}
|
|
788
795
|
|
|
@@ -1752,6 +1759,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
1752
1759
|
h('div', { className: 'tacit-progress-text' }, autoOn ? t('status.autoHint') : t('status.autoOff')),
|
|
1753
1760
|
hasTrend
|
|
1754
1761
|
? h('div', { className: 'tacit-trend' },
|
|
1762
|
+
h('span', { className: 'tacit-chip' }, t('status.trendCorrections', { a: pct(trend.early.correctionRate), b: pct(trend.recent.correctionRate) })),
|
|
1755
1763
|
h('span', { className: 'tacit-chip' }, t('status.trendMessy', { a: pct(trend.early.messyRate), b: pct(trend.recent.messyRate) })),
|
|
1756
1764
|
h('span', { className: 'tacit-chip' }, t('status.trendTokens', { a: fmt(trend.early.tokensPerTurn), b: fmt(trend.recent.tokensPerTurn) })),
|
|
1757
1765
|
h('span', { className: 'tacit-progress-text' }, t('status.trendHint', { n: String(trend.window) })))
|
|
@@ -2253,6 +2261,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2253
2261
|
*/
|
|
2254
2262
|
let confirmOpener = null
|
|
2255
2263
|
|
|
2264
|
+
/**
|
|
2265
|
+
* The two focus stops of the dialog, addressed by id rather than by ref:
|
|
2266
|
+
* refs would add hooks to a component whose hook count must not move.
|
|
2267
|
+
*/
|
|
2268
|
+
const CONFIRM_CANCEL_ID = 'tacit-confirm-cancel'
|
|
2269
|
+
const CONFIRM_ACCEPT_ID = 'tacit-confirm-accept'
|
|
2270
|
+
|
|
2256
2271
|
function captureConfirmOpener() {
|
|
2257
2272
|
confirmOpener = typeof document !== 'undefined' && document !== null ? document.activeElement : null
|
|
2258
2273
|
}
|
|
@@ -2281,10 +2296,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2281
2296
|
return h('div', {
|
|
2282
2297
|
className: 'tacit-modal-backdrop',
|
|
2283
2298
|
onClick: cancel,
|
|
2284
|
-
// Escape
|
|
2285
|
-
// bubbles
|
|
2299
|
+
// Escape and Tab are both handled here: a keydown from either button
|
|
2300
|
+
// bubbles to the backdrop. With exactly two stops in the dialog,
|
|
2301
|
+
// forward and backward tabbing both land on the other button, so
|
|
2302
|
+
// `shiftKey` needs no branch of its own.
|
|
2286
2303
|
onKeyDown: (event) => {
|
|
2287
|
-
if (event.key === 'Escape')
|
|
2304
|
+
if (event.key === 'Escape') {
|
|
2305
|
+
// Stopped here: the host's Settings modal also closes on Escape, and
|
|
2306
|
+
// cancelling a confirm must not take the whole settings page with it.
|
|
2307
|
+
if (typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
2308
|
+
cancel()
|
|
2309
|
+
return
|
|
2310
|
+
}
|
|
2311
|
+
if (event.key !== 'Tab' || typeof document === 'undefined' || document === null) return
|
|
2312
|
+
const cancelButton = document.getElementById(CONFIRM_CANCEL_ID)
|
|
2313
|
+
const acceptButton = document.getElementById(CONFIRM_ACCEPT_ID)
|
|
2314
|
+
if (cancelButton === null || acceptButton === null) return
|
|
2315
|
+
const next = document.activeElement === acceptButton ? cancelButton : acceptButton
|
|
2316
|
+
event.preventDefault()
|
|
2317
|
+
if (typeof next.focus === 'function') next.focus()
|
|
2288
2318
|
},
|
|
2289
2319
|
},
|
|
2290
2320
|
h('div', {
|
|
@@ -2298,8 +2328,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2298
2328
|
h('h3', { className: 'tacit-modal-title', id: 'tacit-confirm-title' }, title),
|
|
2299
2329
|
h('p', { className: 'tacit-confirm-body' }, body),
|
|
2300
2330
|
h('div', { className: 'tacit-confirm-actions' },
|
|
2301
|
-
h('button', { type: 'button', className: 'tacit-btn', autoFocus: true, onClick: cancel }, t('confirm.cancel')),
|
|
2302
|
-
h('button', { type: 'button', className: 'tacit-btn tacit-btn-danger', onClick: onConfirm }, confirmLabel))))
|
|
2331
|
+
h('button', { type: 'button', id: CONFIRM_CANCEL_ID, className: 'tacit-btn', autoFocus: true, onClick: cancel }, t('confirm.cancel')),
|
|
2332
|
+
h('button', { type: 'button', id: CONFIRM_ACCEPT_ID, className: 'tacit-btn tacit-btn-danger', onClick: onConfirm }, confirmLabel))))
|
|
2303
2333
|
}
|
|
2304
2334
|
|
|
2305
2335
|
// ── Global panel (Settings → Tacit section) ────────────────────────────
|
|
@@ -2362,14 +2392,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2362
2392
|
typeof entry.workspace === 'string' && entry.workspace.length > 0
|
|
2363
2393
|
? h('span', { className: 'tacit-chip tacit-chip-scope', title: entry.workspace }, t('steer.workspace', { name: labelOf(entry.workspace) }))
|
|
2364
2394
|
: null,
|
|
2365
|
-
entry.status === '
|
|
2366
|
-
? h('span', { className: 'tacit-chip tacit-chip-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
:
|
|
2395
|
+
entry.status === 'queued'
|
|
2396
|
+
? h('span', { className: 'tacit-chip tacit-chip-muted' }, t('steer.queued'))
|
|
2397
|
+
: entry.status === 'candidate'
|
|
2398
|
+
? h('span', { className: 'tacit-chip tacit-chip-trial' }, t('steer.trial', {
|
|
2399
|
+
n: String(entry.trial !== null && typeof entry.trial === 'object' && typeof entry.trial.turns === 'number' ? entry.trial.turns : 0),
|
|
2400
|
+
total: String(config !== null && typeof config.directiveTrialTurns === 'number' ? config.directiveTrialTurns : 10),
|
|
2401
|
+
}))
|
|
2402
|
+
: entry.status === 'retired'
|
|
2403
|
+
? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('steer.retired', { reason: String(entry.retiredReason || '') }))
|
|
2404
|
+
: h('span', { className: 'tacit-chip tacit-chip-ok' }, t('steer.active')),
|
|
2373
2405
|
h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))))),
|
|
2374
2406
|
h('div', { className: 'tacit-settings-row' },
|
|
2375
2407
|
h('input', {
|
|
@@ -2515,9 +2547,15 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2515
2547
|
const confirm = rootStore.confirm !== null && typeof rootStore.confirm === 'object' && rootStore.confirm.kind === 'usage'
|
|
2516
2548
|
? 'usage'
|
|
2517
2549
|
: (rootStore.confirm !== null && typeof rootStore.confirm === 'object' && rootStore.confirm.kind === 'reports' ? 'reports' : null)
|
|
2518
|
-
const retention = config !== null && typeof config.costHistoryDays === 'number' &&
|
|
2550
|
+
const retention = config !== null && typeof config.costHistoryDays === 'number' && Number.isFinite(config.costHistoryDays) && config.costHistoryDays > 0
|
|
2519
2551
|
? config.costHistoryDays
|
|
2520
2552
|
: 30
|
|
2553
|
+
// A value set by hand in the YAML need not be one of the offered days.
|
|
2554
|
+
// It gets an option of its own, in place, rather than being displayed
|
|
2555
|
+
// as a neighbour it is not.
|
|
2556
|
+
const retentionDays = RETENTION_DAYS.includes(retention)
|
|
2557
|
+
? RETENTION_DAYS
|
|
2558
|
+
: [...RETENTION_DAYS, retention].sort((a, b) => a - b)
|
|
2521
2559
|
/** Every card is titled by `card.<id>` and driven by `rootStore.sections`. */
|
|
2522
2560
|
const card = (id, children, extra) => SectionCard(kit, {
|
|
2523
2561
|
id,
|
|
@@ -2584,7 +2622,6 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2584
2622
|
card('pricing', [
|
|
2585
2623
|
PricingCard(kit, {
|
|
2586
2624
|
pricing: usagePricing,
|
|
2587
|
-
open: sections.pricing === true,
|
|
2588
2625
|
refreshing: rootStore.pricingRefreshing === true,
|
|
2589
2626
|
onRefresh: () => refreshPricing(t),
|
|
2590
2627
|
}),
|
|
@@ -2661,7 +2698,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2661
2698
|
value: String(retention),
|
|
2662
2699
|
onChange: (event) => updateRootConfig({ costHistoryDays: Number(event.target.value) }),
|
|
2663
2700
|
},
|
|
2664
|
-
...
|
|
2701
|
+
...retentionDays.map((days) => h('option', { key: days, value: String(days) }, String(days))))),
|
|
2665
2702
|
h('div', { className: 'tacit-settings-row' },
|
|
2666
2703
|
h('label', { className: 'tacit-settings-label', htmlFor: 'tacit-warn-daily' }, t('privacy.warnDaily')),
|
|
2667
2704
|
h('input', {
|
|
@@ -2678,7 +2715,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2678
2715
|
className: 'tacit-btn tacit-btn-sm',
|
|
2679
2716
|
// Two identically labelled buttons in one card: the accessible
|
|
2680
2717
|
// name has to say which threshold each one applies.
|
|
2681
|
-
'aria-label': t('privacy.
|
|
2718
|
+
'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnDaily') }),
|
|
2682
2719
|
onClick: () => applyWarn('costWarnDailyUsd', dailyText, setDailyText),
|
|
2683
2720
|
}, t('privacy.apply'))),
|
|
2684
2721
|
h('div', { className: 'tacit-settings-row' },
|
|
@@ -2697,7 +2734,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2697
2734
|
className: 'tacit-btn tacit-btn-sm',
|
|
2698
2735
|
// Two identically labelled buttons in one card: the accessible
|
|
2699
2736
|
// name has to say which threshold each one applies.
|
|
2700
|
-
'aria-label': t('privacy.
|
|
2737
|
+
'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnMonthly') }),
|
|
2701
2738
|
onClick: () => applyWarn('costWarnMonthlyUsd', monthlyText, setMonthlyText),
|
|
2702
2739
|
}, t('privacy.apply'))),
|
|
2703
2740
|
h('p', { className: 'tacit-panel-hint' }, t('privacy.warnHint')),
|
|
@@ -2744,7 +2781,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2744
2781
|
/** Every run type and op the ledger can tag, in the order the filters list them. */
|
|
2745
2782
|
const USAGE_RUN_TYPES = ['bootstrap', 'analysis', 'analysis-batch', 'improve', 'directive-distillation', 'style-distillation', 'prompt-enrichment']
|
|
2746
2783
|
const USAGE_RANGES = ['today', '7d', '30d', 'month', 'all']
|
|
2747
|
-
const USAGE_STATUSES = ['success', 'partial', 'failed']
|
|
2784
|
+
const USAGE_STATUSES = ['running', 'success', 'partial', 'failed']
|
|
2748
2785
|
const USAGE_COLUMNS = ['time', 'op', 'scope', 'model', 'status', 'calls', 'tokens', 'cost']
|
|
2749
2786
|
|
|
2750
2787
|
/** One label/value tile; `note` is the small "n unpriced" line under the value. */
|
|
@@ -2942,7 +2979,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2942
2979
|
? run.attempts.filter((attempt) => attempt !== null && typeof attempt === 'object')
|
|
2943
2980
|
: null
|
|
2944
2981
|
return h('div', { className: 'tacit-usage-attempts', id: 'tacit-run-' + runId, role: 'row' },
|
|
2945
|
-
h('div', { className: 'tacit-usage-cell tacit-usage-attempts-cell', role: 'cell' },
|
|
2982
|
+
h('div', { className: 'tacit-usage-cell tacit-usage-attempts-cell', role: 'cell', 'aria-colspan': USAGE_COLUMNS.length },
|
|
2946
2983
|
attempts === null
|
|
2947
2984
|
? h('p', { className: 'tacit-panel-hint' }, t('usage.loading'))
|
|
2948
2985
|
: attempts.map((attempt) => AttemptRow(kit, attempt))))
|
|
@@ -2951,9 +2988,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
2951
2988
|
/** One run row; the first cell is the disclosure button for its attempts. */
|
|
2952
2989
|
function RunRow(kit, { item, open, onToggleRun }) {
|
|
2953
2990
|
const { t, fmtTime } = kit
|
|
2991
|
+
// A bootstrap run carries no workspace because it covers every session,
|
|
2992
|
+
// which is worth saying; any other run type missing the field is simply
|
|
2993
|
+
// a row with nothing to scope by.
|
|
2994
|
+
const unscoped = item.type === 'bootstrap' ? t('usage.scopeAll') : '—'
|
|
2954
2995
|
const scope = item.workspace.length > 0 && item.turn !== null
|
|
2955
2996
|
? item.workspace + ' · #' + String(item.turn)
|
|
2956
|
-
: (item.workspace.length > 0 ? item.workspace : (item.turn !== null ? '#' + String(item.turn) :
|
|
2997
|
+
: (item.workspace.length > 0 ? item.workspace : (item.turn !== null ? '#' + String(item.turn) : unscoped))
|
|
2957
2998
|
const cell = (key, child) => h('span', { key, className: 'tacit-usage-cell', role: 'cell' }, child)
|
|
2958
2999
|
return h('div', { className: 'tacit-usage-row', role: 'row' },
|
|
2959
3000
|
cell('time', h('button', {
|
|
@@ -3149,13 +3190,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
3149
3190
|
|
|
3150
3191
|
/**
|
|
3151
3192
|
* The Pricing card body. Like `UsageCard` a plain renderer, not a
|
|
3152
|
-
* component: it owns no hooks and no state. It
|
|
3153
|
-
*
|
|
3154
|
-
*
|
|
3193
|
+
* component: it owns no hooks and no state. It renders whether or not its
|
|
3194
|
+
* card is open, because the `SectionCard` around it hides a collapsed body
|
|
3195
|
+
* rather than unmounting it, which is what keeps find-in-page working over
|
|
3196
|
+
* the rate table.
|
|
3155
3197
|
*/
|
|
3156
|
-
function PricingCard(kit, { pricing,
|
|
3198
|
+
function PricingCard(kit, { pricing, onRefresh, refreshing }) {
|
|
3157
3199
|
const { t, fmtTime } = kit
|
|
3158
|
-
if (open !== true) return null
|
|
3159
3200
|
const priced = pricingOf(pricing)
|
|
3160
3201
|
const models = Object.keys(priced.models).sort()
|
|
3161
3202
|
const when = priced.refreshedAt > 0
|
|
@@ -3224,6 +3265,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
3224
3265
|
+ '.tacit-settings{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;margin-bottom:12px;display:flex;flex-direction:column;gap:8px}'
|
|
3225
3266
|
+ '.tacit-settings-title{font-size:12px;font-weight:600}.tacit-settings-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.tacit-settings-label{font-size:12px;color:var(--dsw-alias-label-secondary);min-width:150px}'
|
|
3226
3267
|
+ '.tacit-settings-notice{font-size:11px;color:var(--dsw-alias-state-success-primary)}'
|
|
3268
|
+
// The idle live region has to stay mounted to announce, but as an empty
|
|
3269
|
+
// flex item it still claims one of the panel's 10px gaps; -5px a side
|
|
3270
|
+
// gives that back.
|
|
3271
|
+
+ '.tacit-panel>.tacit-settings-notice:empty{margin-block:-5px}'
|
|
3227
3272
|
+ '.tacit-select,.tacit-input{font:inherit;font-size:12px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:3px 8px}.tacit-input{width:90px}'
|
|
3228
3273
|
+ '.tacit-improve-btn{font:inherit;font-size:13px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-secondary);background:transparent;border:0;border-radius:24px;cursor:pointer;height:28px;padding:0 8px;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.tacit-improve-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}'
|
|
3229
3274
|
+ '.tacit-check{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);flex:none;margin:2px 4px 0 0}'
|
|
@@ -3398,6 +3443,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
|
|
|
3398
3443
|
clearUsageHistory,
|
|
3399
3444
|
runNotice,
|
|
3400
3445
|
fmtRate,
|
|
3446
|
+
ConfirmDialog,
|
|
3401
3447
|
UsageCard,
|
|
3402
3448
|
PricingCard,
|
|
3403
3449
|
},
|
package/lib/analyze.js
CHANGED
|
@@ -114,6 +114,7 @@ export const DIRECTIVE_TOOL = {
|
|
|
114
114
|
items: {
|
|
115
115
|
type: 'object',
|
|
116
116
|
properties: {
|
|
117
|
+
id: { type: 'string', description: 'the [id] of the current directive this one keeps or rewords; omit for a genuinely new directive' },
|
|
117
118
|
text: { type: 'string', maxLength: 220, description: 'one sentence, at most 25 words' },
|
|
118
119
|
workspace: { type: 'string', description: 'only when the habit shows up in exactly one workspace: that workspace name as written in the evidence' },
|
|
119
120
|
},
|
|
@@ -125,6 +126,67 @@ export const DIRECTIVE_TOOL = {
|
|
|
125
126
|
},
|
|
126
127
|
}
|
|
127
128
|
export const MAX_WORKSPACE_DIRECTIVES = 4
|
|
129
|
+
/** Retired directives kept on the profile so the distiller is told not to re-propose them. */
|
|
130
|
+
export const MAX_RETIRED = 6
|
|
131
|
+
|
|
132
|
+
/** The workspace a directive is limited to; '' = every conversation. */
|
|
133
|
+
export const scopeOf = (entry) => (typeof entry.workspace === 'string' && entry.workspace.length > 0 ? entry.workspace : '')
|
|
134
|
+
const directiveKey = (scope, text) => scope + '\n' + text.trim().toLowerCase()
|
|
135
|
+
|
|
136
|
+
/** At most MAX_DIRECTIVES global and MAX_WORKSPACE_DIRECTIVES per-workspace live directives, plus the last MAX_RETIRED retired ones; order kept. */
|
|
137
|
+
export function capDirectives(list) {
|
|
138
|
+
const retired = list.filter((entry) => entry.status === 'retired')
|
|
139
|
+
const forgotten = new Set(retired.slice(0, Math.max(0, retired.length - MAX_RETIRED)))
|
|
140
|
+
const counts = new Map()
|
|
141
|
+
return list.filter((entry) => {
|
|
142
|
+
if (entry.status === 'retired') return !forgotten.has(entry)
|
|
143
|
+
const scope = scopeOf(entry)
|
|
144
|
+
const n = counts.get(scope) ?? 0
|
|
145
|
+
if (n >= (scope === '' ? MAX_DIRECTIVES : MAX_WORKSPACE_DIRECTIVES)) return false
|
|
146
|
+
counts.set(scope, n + 1)
|
|
147
|
+
return true
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merge the model's new complete set of directives ({ id?, text, workspace? })
|
|
153
|
+
* into the profile: user entries are untouched; the global distilled set and
|
|
154
|
+
* the distilled set of every workspace the model mentioned are replaced;
|
|
155
|
+
* distilled entries of other workspaces are kept (their evidence was not in
|
|
156
|
+
* this batch), and so are retired ones (the do-not-re-propose list). An item
|
|
157
|
+
* naming an existing id, or repeating an existing text, keeps that entry's
|
|
158
|
+
* identity, state, trial and enabled flag and only updates the text; a
|
|
159
|
+
* retired entry stays exactly as it is; a new one queues for its trial slot.
|
|
160
|
+
*/
|
|
161
|
+
export function mergeDirectives(profile, items, { nextId }) {
|
|
162
|
+
const users = profile.directives.filter((entry) => entry.source === 'user')
|
|
163
|
+
const userKeys = new Set(users.map((entry) => directiveKey(scopeOf(entry), entry.text)))
|
|
164
|
+
const prior = profile.directives.filter((entry) => entry.source !== 'user')
|
|
165
|
+
const byId = new Map(prior.map((entry) => [entry.id, entry]))
|
|
166
|
+
const byKey = new Map(prior.map((entry) => [directiveKey(scopeOf(entry), entry.text), entry]))
|
|
167
|
+
const mentioned = new Set([''])
|
|
168
|
+
for (const item of items) mentioned.add(scopeOf(item))
|
|
169
|
+
const distilled = []
|
|
170
|
+
const seen = new Set()
|
|
171
|
+
const matched = new Set()
|
|
172
|
+
for (const item of items) {
|
|
173
|
+
const scope = scopeOf(item)
|
|
174
|
+
const key = directiveKey(scope, item.text)
|
|
175
|
+
if (seen.has(key) || userKeys.has(key)) continue
|
|
176
|
+
seen.add(key)
|
|
177
|
+
const kept = (typeof item.id === 'string' ? byId.get(item.id) : undefined) ?? byKey.get(key)
|
|
178
|
+
if (kept !== undefined) {
|
|
179
|
+
if (kept.status === 'retired' || matched.has(kept.id)) continue
|
|
180
|
+
matched.add(kept.id)
|
|
181
|
+
distilled.push({ ...kept, text: item.text })
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
distilled.push({ id: nextId(), text: item.text, enabled: true, source: 'distilled', createdAt: Date.now(), status: 'queued', ...(scope === '' ? {} : { workspace: scope }) })
|
|
185
|
+
}
|
|
186
|
+
const untouched = prior.filter((entry) => !matched.has(entry.id) && (entry.status === 'retired' || !mentioned.has(scopeOf(entry))))
|
|
187
|
+
profile.directives = capDirectives([...users, ...distilled, ...untouched])
|
|
188
|
+
return profile
|
|
189
|
+
}
|
|
128
190
|
|
|
129
191
|
/** The last path segment of a workspace directory — what a person calls the project. */
|
|
130
192
|
export function workspaceLabel(cwd) {
|
|
@@ -201,23 +263,36 @@ export function normalizeEnrichNote(text) {
|
|
|
201
263
|
|
|
202
264
|
// ── Measured trend ─────────────────────────────────────────────────────────
|
|
203
265
|
|
|
266
|
+
/**
|
|
267
|
+
* One conversation's turns, each with `corrected`: whether the user's NEXT
|
|
268
|
+
* message in that conversation reads as a correction of it. The last turn has
|
|
269
|
+
* no next message and counts as not corrected.
|
|
270
|
+
*/
|
|
271
|
+
export function markCorrections(turns) {
|
|
272
|
+
const list = (Array.isArray(turns) ? turns : []).filter((turn) => turn !== null && typeof turn === 'object')
|
|
273
|
+
return list.map((turn, index) => ({ ...turn, corrected: index + 1 < list.length && looksLikeCorrection(list[index + 1].prompt) }))
|
|
274
|
+
}
|
|
275
|
+
|
|
204
276
|
function windowStats(turns) {
|
|
205
277
|
const n = turns.length
|
|
206
|
-
if (n === 0) return { n: 0, messyRate: 0, tokensPerTurn: 0 }
|
|
278
|
+
if (n === 0) return { n: 0, correctionRate: 0, messyRate: 0, tokensPerTurn: 0 }
|
|
279
|
+
let corrected = 0
|
|
207
280
|
let messy = 0
|
|
208
281
|
let tokens = 0
|
|
209
282
|
for (const turn of turns) {
|
|
283
|
+
if (turn.corrected === true) corrected += 1
|
|
210
284
|
if (isMessyTurn(turn, { minSteps: Number.POSITIVE_INFINITY })) messy += 1
|
|
211
285
|
const usage = turn.usage !== null && typeof turn.usage === 'object' ? turn.usage : {}
|
|
212
286
|
const read = (key) => (typeof usage[key] === 'number' && Number.isFinite(usage[key]) ? usage[key] : 0)
|
|
213
287
|
tokens += read('inputTokens') + read('outputTokens') + read('cacheReadTokens') + read('reasoningTokens')
|
|
214
288
|
}
|
|
215
|
-
return { n, messyRate: messy / n, tokensPerTurn: Math.round(tokens / n) }
|
|
289
|
+
return { n, correctionRate: corrected / n, messyRate: messy / n, tokensPerTurn: Math.round(tokens / n) }
|
|
216
290
|
}
|
|
217
291
|
|
|
218
292
|
/**
|
|
219
293
|
* Real before/after numbers from the fold: the first `window` finished turns
|
|
220
|
-
* vs. the latest `window`, on
|
|
294
|
+
* vs. the latest `window`, on how often the user corrected the agent (turns
|
|
295
|
+
* pre-marked by `markCorrections`), rework signals (retries/errors/compactions/
|
|
221
296
|
* rejections — step counts are deliberately NOT a signal) and tokens/turn.
|
|
222
297
|
*/
|
|
223
298
|
export function computeTrend(turns, { window = 20 } = {}) {
|
|
@@ -477,7 +552,9 @@ export const DIRECTIVE_SYSTEM_PROMPT = [
|
|
|
477
552
|
'mood, addressed to the agent. A directive that needs a second sentence is two',
|
|
478
553
|
'directives or too specific. You are writing',
|
|
479
554
|
'the COMPLETE new set: keep existing directives that still hold (reworded if',
|
|
480
|
-
'sharper), drop ones that were one-off, add what is
|
|
555
|
+
'sharper, returned with their id), drop ones that were one-off, add what is',
|
|
556
|
+
'missing. Never re-propose a retired directive or a close rephrasing of one.',
|
|
557
|
+
'No preamble.',
|
|
481
558
|
].join('\n')
|
|
482
559
|
|
|
483
560
|
export function buildDirectiveUserText(profile, recentReports = [], { labelOf = workspaceLabel } = {}) {
|
|
@@ -523,10 +600,19 @@ export function buildDirectiveUserText(profile, recentReports = [], { labelOf =
|
|
|
523
600
|
lines.push('', '=== STYLE RULES THE USER CONFIRMED ===')
|
|
524
601
|
for (const rule of rules) lines.push('- ' + clipSafe(rule.rule, 300))
|
|
525
602
|
}
|
|
526
|
-
const
|
|
603
|
+
const directives = Array.isArray(profile?.directives) ? profile.directives.filter((entry) => typeof entry?.text === 'string' && entry.text.length > 0) : []
|
|
604
|
+
const directiveLine = (entry, withId) => '- ' + (withId && typeof entry.id === 'string' ? '[' + entry.id + '] ' : '') + clipDirective(entry.text)
|
|
605
|
+
+ (typeof entry.workspace === 'string' && entry.workspace.length > 0 ? ' [workspace: ' + labelOf(entry.workspace) + ']' : '')
|
|
606
|
+
const existing = directives.filter((entry) => entry.status !== 'retired')
|
|
527
607
|
if (existing.length > 0) {
|
|
528
608
|
lines.push('', '=== CURRENT DIRECTIVES (keep the ones that still hold) ===')
|
|
529
|
-
for (const entry of existing) lines.push(
|
|
609
|
+
for (const entry of existing) lines.push(directiveLine(entry, true))
|
|
610
|
+
lines.push('When you keep or reword one of these, return it with its [id]; leave id out only for a genuinely new directive.')
|
|
611
|
+
}
|
|
612
|
+
const retired = directives.filter((entry) => entry.status === 'retired').slice(-MAX_RETIRED)
|
|
613
|
+
if (retired.length > 0) {
|
|
614
|
+
lines.push('', '=== RETIRED (made things worse while active; do not re-propose these) ===')
|
|
615
|
+
for (const entry of retired) lines.push(directiveLine(entry, false))
|
|
530
616
|
}
|
|
531
617
|
lines.push('', 'Write 2-4 directives for the agent about this user. A habit the user has', 'shown they can fix themselves is still worth a directive: the agent should', 'compensate for it when it is missing, not ask.')
|
|
532
618
|
return lines.join('\n')
|
|
@@ -536,7 +622,8 @@ export function buildDirectiveUserText(profile, recentReports = [], { labelOf =
|
|
|
536
622
|
const ASKS_USER_RE = /\b(ask|confirm with|check with|clarify with|verify with|get (approval|confirmation|permission) from) (the )?user\b|\bstop and ask\b|\bbefore doing anything\b|\bask (what|which|whether|if|for)\b/i
|
|
537
623
|
|
|
538
624
|
/**
|
|
539
|
-
* Parse a directives payload into clipped, deduped one-liners (≤4 kept)
|
|
625
|
+
* Parse a directives payload into clipped, deduped one-liners (≤4 kept), each
|
|
626
|
+
* with the id of the current directive it restates when the model gave one;
|
|
540
627
|
* directives that instruct the agent to ask the user are rejected.
|
|
541
628
|
*/
|
|
542
629
|
export function classifyDirectives(text) {
|
|
@@ -554,6 +641,7 @@ export function classifyDirectives(text) {
|
|
|
554
641
|
const workspace = typeof item === 'object' && item !== null && typeof item.workspace === 'string' && item.workspace.trim().length > 0
|
|
555
642
|
? clipSafe(item.workspace.trim(), 200)
|
|
556
643
|
: undefined
|
|
644
|
+
const id = typeof item === 'object' && item !== null && typeof item.id === 'string' && item.id.trim().length > 0 ? clipSafe(item.id.trim(), 64) : undefined
|
|
557
645
|
const key = (workspace ?? '') + '\n' + value.toLowerCase()
|
|
558
646
|
if (value.length === 0 || seen.has(key)) continue
|
|
559
647
|
seen.add(key)
|
|
@@ -561,7 +649,7 @@ export function classifyDirectives(text) {
|
|
|
561
649
|
rejected.push(value)
|
|
562
650
|
continue
|
|
563
651
|
}
|
|
564
|
-
kept.push(workspace === undefined ? {
|
|
652
|
+
kept.push({ text: value, ...(workspace === undefined ? {} : { workspace }), ...(id === undefined ? {} : { id }) })
|
|
565
653
|
if (kept.length >= 4) break
|
|
566
654
|
}
|
|
567
655
|
return { kept, rejected }
|
|
@@ -574,14 +662,13 @@ export function classifyDirectives(text) {
|
|
|
574
662
|
* nothing is enabled (an empty section contributes nothing).
|
|
575
663
|
*/
|
|
576
664
|
export function buildSteeringSection(profile, { cwd } = {}) {
|
|
577
|
-
const here = typeof cwd === 'string' && cwd.length > 0 ? cwd :
|
|
578
|
-
const scopeOf = (entry) => (typeof entry.workspace === 'string' && entry.workspace.length > 0 ? entry.workspace : null)
|
|
665
|
+
const here = typeof cwd === 'string' && cwd.length > 0 ? cwd : ''
|
|
579
666
|
const candidates = (Array.isArray(profile?.directives) ? profile.directives : [])
|
|
580
|
-
.filter((entry) => entry !== null && typeof entry === 'object' && entry.enabled !== false && entry.status !== 'retired'
|
|
667
|
+
.filter((entry) => entry !== null && typeof entry === 'object' && entry.enabled !== false && entry.status !== 'retired' && entry.status !== 'queued'
|
|
581
668
|
&& typeof entry.text === 'string' && entry.text.trim().length > 0)
|
|
582
|
-
.filter((entry) => scopeOf(entry) ===
|
|
669
|
+
.filter((entry) => scopeOf(entry) === '' || scopeOf(entry) === here)
|
|
583
670
|
// This workspace's own directives first: they are the more specific ones.
|
|
584
|
-
const enabled = [...candidates.filter((entry) => scopeOf(entry) !==
|
|
671
|
+
const enabled = [...candidates.filter((entry) => scopeOf(entry) !== ''), ...candidates.filter((entry) => scopeOf(entry) === '')]
|
|
585
672
|
if (enabled.length === 0) return { text: '', ids: [] }
|
|
586
673
|
const header = [
|
|
587
674
|
'## About this user (learned by Tacit from their past prompts)',
|
package/lib/index.js
CHANGED
|
@@ -87,7 +87,7 @@ export function apply(ctx, config) {
|
|
|
87
87
|
const directives = store.profile().directives
|
|
88
88
|
const count = (status) => directives.filter((entry) => entry.status === status).length
|
|
89
89
|
console.info('[tacit] loaded — directives: ' + count('active') + ' active, ' + count('candidate') + ' candidates, '
|
|
90
|
-
+ count('retired') + ' retired; steering ' + (cfg.steerAgent ? 'on' : 'off') + '; auto-analysis '
|
|
90
|
+
+ count('queued') + ' queued, ' + count('retired') + ' retired; steering ' + (cfg.steerAgent ? 'on' : 'off') + '; auto-analysis '
|
|
91
91
|
+ (cfg.autoAnalyze ? 'on (cap ' + cfg.autoDailyBudget + '/day)' : 'off'))
|
|
92
92
|
} catch {
|
|
93
93
|
// Logging must never keep the plugin from loading.
|
package/lib/schema.js
CHANGED
|
@@ -29,7 +29,7 @@ export const COACH_PROVIDER = 'deepseek-official'
|
|
|
29
29
|
* outside this list onto one of these. Every entry has an `err.*` key in both
|
|
30
30
|
* dictionaries (`client/src/10-i18n.js`, test-enforced).
|
|
31
31
|
*/
|
|
32
|
-
export const COACH_ERROR_CODES = ['no-llm', 'no-api-key', 'rate-limited', 'timeout', 'empty-response', 'call-failed']
|
|
32
|
+
export const COACH_ERROR_CODES = ['no-llm', 'no-api-key', 'no-credit', 'rate-limited', 'timeout', 'empty-response', 'call-failed']
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The loader-facing plugin config. Wrapped in `z.preprocess` so a patch row
|
|
@@ -232,15 +232,23 @@ const feedbackEntrySchema = z.object({
|
|
|
232
232
|
* system prompt). `distilled` entries come from analyses; `user` entries are
|
|
233
233
|
* typed in Settings and survive every distillation.
|
|
234
234
|
*/
|
|
235
|
-
const directiveTrialSchema = z.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
235
|
+
const directiveTrialSchema = z.preprocess(
|
|
236
|
+
// Profiles written before corrections were graded named the messy baseline `baselineRate`.
|
|
237
|
+
(raw) => (raw !== null && typeof raw === 'object' && 'baselineRate' in raw && !('baselineMessyRate' in raw) ? { ...raw, baselineMessyRate: raw.baselineRate } : raw),
|
|
238
|
+
z.object({
|
|
239
|
+
/** Finished turns observed while the candidate was injected. */
|
|
240
|
+
turns: z.number().int().min(0),
|
|
241
|
+
/** How many of those were messy. */
|
|
242
|
+
messy: z.number().int().min(0),
|
|
243
|
+
/** How many of those the user corrected with their next message. */
|
|
244
|
+
corrected: z.number().int().min(0).default(0),
|
|
245
|
+
/** Messy-turn rate over the 20 turns before the trial started. */
|
|
246
|
+
baselineMessyRate: z.number(),
|
|
247
|
+
/** Correction rate over those 20 turns; -1 = unknown (older profile), recomputed when the trial next advances. */
|
|
248
|
+
baselineCorrectionRate: z.number().default(-1),
|
|
249
|
+
startedAt: z.number(),
|
|
250
|
+
}),
|
|
251
|
+
)
|
|
244
252
|
|
|
245
253
|
const directiveSchema = z.object({
|
|
246
254
|
id: z.string(),
|
|
@@ -248,8 +256,8 @@ const directiveSchema = z.object({
|
|
|
248
256
|
enabled: z.boolean().default(true),
|
|
249
257
|
source: z.enum(['distilled', 'user']).default('distilled'),
|
|
250
258
|
createdAt: z.number(),
|
|
251
|
-
/** candidate = injected on trial; active = proven (or user-made); retired = made things worse. */
|
|
252
|
-
status: z.enum(['candidate', 'active', 'retired']).default('active'),
|
|
259
|
+
/** queued = waiting for its scope's trial slot; candidate = injected on trial; active = proven (or user-made); retired = made things worse. */
|
|
260
|
+
status: z.enum(['queued', 'candidate', 'active', 'retired']).default('active'),
|
|
253
261
|
trial: directiveTrialSchema.optional(),
|
|
254
262
|
retiredReason: z.string().optional(),
|
|
255
263
|
/** Absolute workspace directory this directive is limited to; absent = every conversation. */
|
|
@@ -347,6 +355,12 @@ export const USAGE_OPS = [
|
|
|
347
355
|
'enrichment',
|
|
348
356
|
]
|
|
349
357
|
|
|
358
|
+
/** Every state one recorded attempt can end in. */
|
|
359
|
+
export const USAGE_ATTEMPT_STATUSES = ['ok', 'failed', 'unmetered']
|
|
360
|
+
|
|
361
|
+
/** Every state a run can be in. `running` is written too: the flush persists live runs. */
|
|
362
|
+
export const USAGE_RUN_STATUSES = ['running', 'success', 'partial', 'failed']
|
|
363
|
+
|
|
350
364
|
/** Every kind of run the tracker groups attempts into. */
|
|
351
365
|
export const USAGE_RUN_TYPES = [
|
|
352
366
|
'bootstrap',
|
|
@@ -382,7 +396,7 @@ export const usageAttemptSchema = z.object({
|
|
|
382
396
|
provider: z.string().default(''),
|
|
383
397
|
reasoningEffort: z.string().nullable().default(null),
|
|
384
398
|
finish: z.string().default(''),
|
|
385
|
-
status: z.enum(
|
|
399
|
+
status: z.enum(USAGE_ATTEMPT_STATUSES),
|
|
386
400
|
code: z.string().default(''),
|
|
387
401
|
sessionId: z.string().default(''),
|
|
388
402
|
turn: z.number().nullable().default(null),
|
|
@@ -425,7 +439,7 @@ export const usageRunSchema = z.object({
|
|
|
425
439
|
trigger: z.string().default(''),
|
|
426
440
|
startedAt: z.number(),
|
|
427
441
|
endedAt: z.number().default(0),
|
|
428
|
-
status: z.enum(
|
|
442
|
+
status: z.enum(USAGE_RUN_STATUSES).default('running'),
|
|
429
443
|
sessionId: z.string().default(''),
|
|
430
444
|
turn: z.number().nullable().default(null),
|
|
431
445
|
workspace: z.string().default(''),
|
|
@@ -466,7 +480,7 @@ export const usageSummarySchema = z.object({
|
|
|
466
480
|
export const usageArgSchema = z.object({
|
|
467
481
|
range: z.enum(['today', '7d', '30d', 'month', 'all']).optional(),
|
|
468
482
|
type: z.enum(USAGE_RUN_TYPES).optional(),
|
|
469
|
-
status: z.enum(
|
|
483
|
+
status: z.enum(USAGE_RUN_STATUSES).optional(),
|
|
470
484
|
model: z.string().max(64).optional(),
|
|
471
485
|
workspace: z.string().max(200).optional(),
|
|
472
486
|
sessionId: z.string().max(200).optional(),
|