dsh-tacit 0.6.0 → 0.6.1

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/analyze.js CHANGED
@@ -210,6 +210,28 @@ export function capDirectives(list, { seenAt = {} } = {}) {
210
210
  return capScopes(capped, seenAt)
211
211
  }
212
212
 
213
+ /**
214
+ * One candidate per scope: the earliest-started trial (then earliest created,
215
+ * then first listed) keeps the slot, every other candidate in that scope goes
216
+ * back to the queue and drops its trial. Idempotent; entries mutate in place.
217
+ */
218
+ export function settleTrialSlots(list) {
219
+ const startOf = (entry) => (typeof entry.trial?.startedAt === 'number' ? entry.trial.startedAt : typeof entry.createdAt === 'number' ? entry.createdAt : Infinity)
220
+ const kept = new Map()
221
+ for (const entry of list) {
222
+ if (entry.status !== 'candidate') continue
223
+ const scope = scopeOf(entry)
224
+ const holder = kept.get(scope)
225
+ if (holder === undefined || startOf(entry) < startOf(holder)) kept.set(scope, entry)
226
+ }
227
+ for (const entry of list) {
228
+ if (entry.status !== 'candidate' || kept.get(scopeOf(entry)) === entry) continue
229
+ entry.status = 'queued'
230
+ delete entry.trial
231
+ }
232
+ return list
233
+ }
234
+
213
235
  /**
214
236
  * Merge the model's new complete set of directives ({ id?, text, workspace? })
215
237
  * into the profile. Three groups, and only the third is replaceable:
@@ -1092,19 +1114,27 @@ export function aggregateProfile(prev, report, maxPatterns, options = {}) {
1092
1114
  const countNew = options.countNew !== false
1093
1115
  const patterns = new Map()
1094
1116
  for (const pattern of Array.isArray(prev?.patterns) ? prev.patterns : []) {
1095
- if (pattern !== null && typeof pattern === 'object' && typeof pattern.kind === 'string') {
1096
- patterns.set(normalizeKind(pattern.kind), {
1097
- kind: normalizeKind(pattern.kind),
1098
- count: typeof pattern.count === 'number' && pattern.count > 0 ? pattern.count : 0,
1099
- lastExample: typeof pattern.lastExample === 'string' ? pattern.lastExample : '',
1100
- applied: counterOf(pattern, 'applied'),
1101
- accepted: counterOf(pattern, 'accepted'),
1102
- rejected: counterOf(pattern, 'rejected'),
1103
- verified: counterOf(pattern, 'verified'),
1104
- unverified: counterOf(pattern, 'unverified'),
1105
- resolved: counterOf(pattern, 'resolved'),
1106
- })
1117
+ if (pattern === null || typeof pattern !== 'object' || typeof pattern.kind !== 'string') continue
1118
+ const kind = normalizeKind(pattern.kind)
1119
+ const next = {
1120
+ kind,
1121
+ count: typeof pattern.count === 'number' && pattern.count > 0 ? pattern.count : 0,
1122
+ lastExample: typeof pattern.lastExample === 'string' ? pattern.lastExample : '',
1123
+ applied: counterOf(pattern, 'applied'),
1124
+ accepted: counterOf(pattern, 'accepted'),
1125
+ rejected: counterOf(pattern, 'rejected'),
1126
+ verified: counterOf(pattern, 'verified'),
1127
+ unverified: counterOf(pattern, 'unverified'),
1128
+ resolved: counterOf(pattern, 'resolved'),
1129
+ }
1130
+ const current = patterns.get(kind)
1131
+ if (current === undefined) {
1132
+ patterns.set(kind, next)
1133
+ continue
1107
1134
  }
1135
+ // Stored spellings that normalise to one kind ('missing context' / 'missing-context') fold into one row.
1136
+ if (next.count > current.count) current.lastExample = next.lastExample
1137
+ for (const field of ['count', 'applied', 'accepted', 'rejected', 'verified', 'unverified', 'resolved']) current[field] += next[field]
1108
1138
  }
1109
1139
  // A good-prompt report says which habits the user overcame on their own this time.
1110
1140
  for (const strength of Array.isArray(report?.strengths) ? report.strengths : []) {
package/lib/service.js CHANGED
@@ -89,6 +89,7 @@ import {
89
89
  renderSteeringSection,
90
90
  scopeOf,
91
91
  capDirectives,
92
+ settleTrialSlots,
92
93
  mergeDirectives,
93
94
  isDeadDirective,
94
95
  MAX_DIRECTIVE_EVIDENCE,
@@ -328,6 +329,7 @@ export function createCoachService(ctx, store, effectiveConfig) {
328
329
  if (workspace.length > 0) entry.workspace = workspace
329
330
  else delete entry.workspace
330
331
  }
332
+ settleTrialSlots(parsed.data.directives)
331
333
  return parsed.data
332
334
  }
333
335
  let directiveSeq = 0
package/lib/usage.js CHANGED
@@ -11,9 +11,10 @@
11
11
  * Two hard rules shape the design:
12
12
  * - **Synchronous.** Every mutation here is plain JS with no `await`, so the
13
13
  * four bootstrap workers calling sinks concurrently interleave safely and
14
- * no model call ever waits on the ledger. Disk writes happen on `endRun`
15
- * and on a debounced, `unref()`'d flush timer that never holds the process
16
- * open.
14
+ * no model call ever waits on the ledger. `endRun` writes the run's day
15
+ * file and the summary before it returns, so a host that exits right after
16
+ * a run loses nothing; attempts of still-live runs reach disk through a
17
+ * debounced, `unref()`'d flush timer that never holds the process open.
17
18
  * - **Content-free.** A run carries ids, counts, tokens and money — never
18
19
  * prompts, responses, tool arguments or full paths (`workspace` is the
19
20
  * label, not the path).
@@ -388,7 +389,7 @@ export function createUsageTracker({ store, config, pricing, now = Date.now, flu
388
389
  return (record) => recordAttempt(runId, tag, record)
389
390
  }
390
391
 
391
- /** Close a run: derive its status, write its day file, evict it, and expire old days. */
392
+ /** Close a run: derive its status, write its day file and the summary, evict it, and expire old days. */
392
393
  function endRun(runId, { results = {}, status } = {}) {
393
394
  const run = live.get(runId)
394
395
  if (run === undefined) return null
@@ -399,6 +400,7 @@ export function createUsageTracker({ store, config, pricing, now = Date.now, flu
399
400
  live.delete(runId)
400
401
  remember(run)
401
402
  pruneIfNewDay()
403
+ flush()
402
404
  return runSummary(runId)
403
405
  }
404
406
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tacit",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Tacit learns what you leave unsaid in your prompts — from messy turns and your own corrections, with zero clicks — and tells the agent how to compensate, on every turn, via a system-prompt section you can read and edit.",
5
5
  "author": "hackernotfound",
6
6
  "license": "MIT",
@@ -42,7 +42,8 @@
42
42
  "check:client": "node scripts/build-client.mjs --check",
43
43
  "check:docs": "node scripts/check-doc-links.mjs",
44
44
  "check:package": "node scripts/check-package.mjs",
45
- "smoke": "node scripts/smoke.mjs"
45
+ "smoke": "node scripts/smoke.mjs",
46
+ "rehearse": "node scripts/rehearse.mjs"
46
47
  },
47
48
  "publishConfig": {
48
49
  "access": "public"