dsh-tacit 0.3.0 → 0.5.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/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
@@ -79,6 +79,8 @@ export const Config = z.preprocess((v) => v ?? {}, z.object({
79
79
  costWarnDailyUsd: z.number().default(0),
80
80
  /** Same as `costWarnDailyUsd`, over a calendar month; 0 disables (clamped in mergeConfig). */
81
81
  costWarnMonthlyUsd: z.number().default(0),
82
+ /** Opt-in: a distilled directive stays queued until *Start trial* is pressed on it. */
83
+ reviewCandidates: z.boolean().default(false),
82
84
  }))
83
85
 
84
86
  /**
@@ -107,6 +109,7 @@ const configPatchSchema = z.object({
107
109
  costHistoryDays: z.number().optional(),
108
110
  costWarnDailyUsd: z.number().optional(),
109
111
  costWarnMonthlyUsd: z.number().optional(),
112
+ reviewCandidates: z.boolean().optional(),
110
113
  })
111
114
 
112
115
  // ── Trajectory projection ──────────────────────────────────────────────────
@@ -232,29 +235,77 @@ const feedbackEntrySchema = z.object({
232
235
  * system prompt). `distilled` entries come from analyses; `user` entries are
233
236
  * typed in Settings and survive every distillation.
234
237
  */
235
- const directiveTrialSchema = z.object({
236
- /** Finished turns observed while the candidate was injected. */
237
- turns: z.number().int().min(0),
238
- /** How many of those were messy. */
239
- messy: z.number().int().min(0),
240
- /** Messy-turn rate over the 20 turns before the trial started. */
241
- baselineRate: z.number(),
242
- startedAt: z.number(),
238
+ const directiveTrialSchema = z.preprocess(
239
+ // Profiles written before corrections were graded named the messy baseline `baselineRate`.
240
+ (raw) => (raw !== null && typeof raw === 'object' && 'baselineRate' in raw && !('baselineMessyRate' in raw) ? { ...raw, baselineMessyRate: raw.baselineRate } : raw),
241
+ z.object({
242
+ /** Finished turns observed while the candidate was injected. */
243
+ turns: z.number().int().min(0),
244
+ /** How many of those were messy. */
245
+ messy: z.number().int().min(0),
246
+ /** How many of those the user corrected with their next message. */
247
+ corrected: z.number().int().min(0).default(0),
248
+ /** Messy-turn rate over the 20 turns before the trial started. */
249
+ baselineMessyRate: z.number(),
250
+ /** Correction rate over those 20 turns; -1 = unknown (older profile), recomputed when the trial next advances. */
251
+ baselineCorrectionRate: z.number().default(-1),
252
+ startedAt: z.number(),
253
+ }),
254
+ )
255
+
256
+ /** One analysis report a distillation read: ids only, never the prompt. */
257
+ const directiveEvidenceSchema = z.object({
258
+ sessionId: z.string(),
259
+ turn: z.number(),
260
+ trigger: z.string(),
243
261
  })
244
262
 
245
- const directiveSchema = z.object({
246
- id: z.string(),
247
- text: z.string(),
248
- enabled: z.boolean().default(true),
249
- source: z.enum(['distilled', 'user']).default('distilled'),
250
- 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'),
253
- trial: directiveTrialSchema.optional(),
254
- retiredReason: z.string().optional(),
255
- /** Absolute workspace directory this directive is limited to; absent = every conversation. */
256
- workspace: z.string().optional(),
257
- })
263
+ /**
264
+ * queued = waiting for its scope's trial slot (or, under `reviewCandidates`, for the user's *Start trial*);
265
+ * candidate = injected on trial; active = proven (or user-made); retired = made things worse;
266
+ * removed = deleted by the user. Retired and removed records are kept so the distiller is told
267
+ * not to propose them again.
268
+ */
269
+ export const DIRECTIVE_STATUSES = ['queued', 'candidate', 'active', 'retired', 'removed']
270
+
271
+ const directiveSchema = z.preprocess(
272
+ (raw) => {
273
+ if (raw === null || typeof raw !== 'object') return raw
274
+ const entry = { ...raw }
275
+ // Profiles written before provenance was recorded: the text is as old as the record.
276
+ if (typeof entry.updatedAt !== 'number') entry.updatedAt = entry.createdAt
277
+ // A candidate is always enabled: switched off, it leaves the trial slot and queues again.
278
+ if (entry.status === 'candidate' && entry.enabled === false) {
279
+ entry.status = 'queued'
280
+ delete entry.trial
281
+ }
282
+ return entry
283
+ },
284
+ z.object({
285
+ id: z.string(),
286
+ text: z.string(),
287
+ enabled: z.boolean().default(true),
288
+ source: z.enum(['distilled', 'user']).default('distilled'),
289
+ createdAt: z.number(),
290
+ /** Last time the text changed (a reword) or the record moved state. */
291
+ updatedAt: z.number(),
292
+ /** Bumped on every reword of the text. */
293
+ version: z.number().int().min(1).default(1),
294
+ status: z.enum(DIRECTIVE_STATUSES).default('active'),
295
+ trial: directiveTrialSchema.optional(),
296
+ retiredReason: z.string().optional(),
297
+ /** Absolute workspace directory this directive is limited to; absent = every conversation. */
298
+ workspace: z.string().optional(),
299
+ /** The reports the distillation that wrote this text read (newest first, capped). */
300
+ evidence: z.array(directiveEvidenceSchema).default([]),
301
+ /** The usage-ledger run of that distillation, so its cost is one lookup away. '' for user-typed. */
302
+ distillationRunId: z.string().default(''),
303
+ /** When the trial reached its verdict; 0 = never. */
304
+ evaluatedAt: z.number().default(0),
305
+ /** When the user pressed *Start trial* under `reviewCandidates`; 0 = not yet. */
306
+ approvedAt: z.number().default(0),
307
+ }),
308
+ )
258
309
 
259
310
  /** The persistent user-wide mistake profile. */
260
311
  export const profileSchema = z.object({
@@ -277,6 +328,8 @@ export const profileSchema = z.object({
277
328
  directives: z.array(directiveSchema).default([]),
278
329
  /** New analyses since the last directive distillation. */
279
330
  analysesSinceDirectives: z.number().int().min(0).default(0),
331
+ /** When a session was last loaded in each directive's workspace, so the least recently seen one is dropped first. */
332
+ workspaceSeenAt: z.record(z.string(), z.number()).default({}),
280
333
  })
281
334
 
282
335
  // ── Route argument codecs ──────────────────────────────────────────────────
@@ -293,9 +346,16 @@ export const statsArgSchema = z.object({
293
346
  export const directivesArgSchema = z.discriminatedUnion('action', [
294
347
  z.object({ action: z.literal('toggle'), id: z.string().min(1).max(64), enabled: z.boolean() }),
295
348
  z.object({ action: z.literal('add'), text: z.string().min(1).max(300), workspace: z.string().max(1000).optional() }),
349
+ z.object({ action: z.literal('rescope'), id: z.string().min(1).max(64), workspace: z.string().max(1000) }),
296
350
  z.object({ action: z.literal('remove'), id: z.string().min(1).max(64) }),
351
+ z.object({ action: z.literal('start-trial'), id: z.string().min(1).max(64) }),
297
352
  ])
298
353
 
354
+ /** `/api/tacit/directive-receipt`: one directive's provenance and cost, by id. */
355
+ export const directiveReceiptArgSchema = z.object({
356
+ id: z.string().min(1).max(64),
357
+ })
358
+
299
359
  // ── Route argument codecs ──────────────────────────────────────────────────
300
360
 
301
361
  export const sessionArgSchema = z.object({
@@ -305,12 +365,15 @@ export const sessionArgSchema = z.object({
305
365
  export const analyzeArgSchema = z.object({
306
366
  sessionId: z.string().min(1).max(200),
307
367
  turn: z.number().int().min(1),
368
+ /** Re-analyze a turn that already has a report (otherwise `already-analyzed`, at no cost). */
369
+ force: z.boolean().optional(),
308
370
  })
309
371
 
310
372
  /** `/api/tacit/analyze-batch`: one session, up to 50 turns analyzed under a single run. */
311
373
  export const analyzeBatchArgSchema = z.object({
312
374
  sessionId: z.string().min(1).max(200),
313
375
  turns: z.array(z.number().int().min(1)).min(1).max(50),
376
+ force: z.boolean().optional(),
314
377
  })
315
378
 
316
379
  export const improveArgSchema = z.object({
@@ -347,6 +410,12 @@ export const USAGE_OPS = [
347
410
  'enrichment',
348
411
  ]
349
412
 
413
+ /** Every state one recorded attempt can end in. */
414
+ export const USAGE_ATTEMPT_STATUSES = ['ok', 'failed', 'unmetered']
415
+
416
+ /** Every state a run can be in. `running` is written too: the flush persists live runs. */
417
+ export const USAGE_RUN_STATUSES = ['running', 'success', 'partial', 'failed']
418
+
350
419
  /** Every kind of run the tracker groups attempts into. */
351
420
  export const USAGE_RUN_TYPES = [
352
421
  'bootstrap',
@@ -382,7 +451,7 @@ export const usageAttemptSchema = z.object({
382
451
  provider: z.string().default(''),
383
452
  reasoningEffort: z.string().nullable().default(null),
384
453
  finish: z.string().default(''),
385
- status: z.enum(['ok', 'failed', 'unmetered']),
454
+ status: z.enum(USAGE_ATTEMPT_STATUSES),
386
455
  code: z.string().default(''),
387
456
  sessionId: z.string().default(''),
388
457
  turn: z.number().nullable().default(null),
@@ -410,10 +479,12 @@ const emptyTokenBuckets = tokenBucketsSchema.parse({})
410
479
  export const usageTotalsSchema = z.object({
411
480
  attempts: z.number().default(0),
412
481
  billedCalls: z.number().default(0),
482
+ failedCalls: z.number().default(0),
413
483
  unmeteredCalls: z.number().default(0),
414
484
  unpricedCalls: z.number().default(0),
415
485
  tokens: tokenBucketsSchema.default(emptyTokenBuckets),
416
486
  usdKnown: z.number().default(0),
487
+ failedUsd: z.number().default(0),
417
488
  })
418
489
 
419
490
  const emptyUsageTotals = usageTotalsSchema.parse({})
@@ -425,7 +496,7 @@ export const usageRunSchema = z.object({
425
496
  trigger: z.string().default(''),
426
497
  startedAt: z.number(),
427
498
  endedAt: z.number().default(0),
428
- status: z.enum(['running', 'success', 'partial', 'failed']).default('running'),
499
+ status: z.enum(USAGE_RUN_STATUSES).default('running'),
429
500
  sessionId: z.string().default(''),
430
501
  turn: z.number().nullable().default(null),
431
502
  workspace: z.string().default(''),
@@ -454,6 +525,8 @@ export const usageSummarySchema = z.object({
454
525
  lifetime: usageTotalsSchema.default(emptyUsageTotals),
455
526
  byType: z.record(usageTotalsSchema).default({}),
456
527
  byModel: z.record(usageTotalsSchema).default({}),
528
+ byProvider: z.record(usageTotalsSchema).default({}),
529
+ byTrigger: z.record(usageTotalsSchema).default({}),
457
530
  days: z.record(usageDayTotalsSchema).default({}),
458
531
  })
459
532
 
@@ -466,7 +539,7 @@ export const usageSummarySchema = z.object({
466
539
  export const usageArgSchema = z.object({
467
540
  range: z.enum(['today', '7d', '30d', 'month', 'all']).optional(),
468
541
  type: z.enum(USAGE_RUN_TYPES).optional(),
469
- status: z.enum(['success', 'partial', 'failed']).optional(),
542
+ status: z.enum(USAGE_RUN_STATUSES).optional(),
470
543
  model: z.string().max(64).optional(),
471
544
  workspace: z.string().max(200).optional(),
472
545
  sessionId: z.string().max(200).optional(),