@lmzhen/dsh-skill-usage 0.1.0-rc.9 → 0.2.0-rc.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/README.md +8 -0
- package/lib/index.js +144 -20
- package/lib/types/index.d.ts +44 -4
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Skill usage telemetry service
|
|
4
4
|
|
|
5
|
+
## Telemetry sources
|
|
6
|
+
|
|
7
|
+
`record(name, kind)` is the write API (`use` / `view` / `patch`). Reads are
|
|
8
|
+
observed automatically: the service listens for `session/event` `tool/call`
|
|
9
|
+
records of the read tools (`skill`, `skill_load`) and bumps `view` on
|
|
10
|
+
EXISTING records only — an arbitrary read never mints a usage record
|
|
11
|
+
(records are authored by skill creation, patching, or curator seeding).
|
|
12
|
+
|
|
5
13
|
## Model Experience
|
|
6
14
|
|
|
7
15
|
### Indirect model surface
|
package/lib/index.js
CHANGED
|
@@ -1,62 +1,186 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { appendEvolutionEvent, bumpPatch, bumpUse, bumpView, eventsFile, evolutionIoAdapter, getRecord, loadUsage, markAgentCreated, mutateUsage, skillsRoot } from "@lmzhen/dsh-evolution-core";
|
|
4
6
|
//#region lib/types/index.js
|
|
5
7
|
/**
|
|
6
8
|
* Skill usage telemetry service for the evolution family.
|
|
7
9
|
* @module @lmzhen/dsh-skill-usage
|
|
8
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Read tool names -> usage kind. The single declarative classification table
|
|
13
|
+
* for read-side observation (A2): any tool listed here records a `view` when
|
|
14
|
+
* its call arguments name a skill.
|
|
15
|
+
*/
|
|
16
|
+
const READ_TOOL_KIND = {
|
|
17
|
+
skill: "view",
|
|
18
|
+
skill_load: "view"
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Skill name from a tool/call arguments payload (A2, mirrors
|
|
22
|
+
* evolution-review's collectReadSkillNames): JSON strings are re-parsed and
|
|
23
|
+
* `name` wins over `skill`. Empty when unparseable or anonymous.
|
|
24
|
+
*/
|
|
25
|
+
function skillNameFromToolCall(raw) {
|
|
26
|
+
let parsed = {};
|
|
27
|
+
if (typeof raw === "string") try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
else if (raw && typeof raw === "object") parsed = raw;
|
|
33
|
+
return typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
|
|
34
|
+
}
|
|
35
|
+
/** Cumulative library-wide usage totals (C observation-window event counts). */
|
|
36
|
+
function usageTotals(map) {
|
|
37
|
+
let views = 0;
|
|
38
|
+
let use = 0;
|
|
39
|
+
let patches = 0;
|
|
40
|
+
for (const record of map.values()) {
|
|
41
|
+
views += record.view_count;
|
|
42
|
+
use += record.use_count;
|
|
43
|
+
patches += record.patch_count;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
skills: map.size,
|
|
47
|
+
views,
|
|
48
|
+
use,
|
|
49
|
+
patches
|
|
50
|
+
};
|
|
51
|
+
}
|
|
9
52
|
var SkillUsageRegistry = class extends Service {
|
|
10
53
|
static inject = ["evolutionIo"];
|
|
11
|
-
static Config = z.object({
|
|
54
|
+
static Config = z.object({
|
|
55
|
+
root: z.string().default(""),
|
|
56
|
+
eventsHome: z.string().default("")
|
|
57
|
+
});
|
|
12
58
|
root;
|
|
59
|
+
eventsHome;
|
|
13
60
|
io;
|
|
14
|
-
usage = null;
|
|
15
61
|
chain = Promise.resolve();
|
|
16
62
|
constructor(ctx, config = {}) {
|
|
17
63
|
super(ctx, "skillUsage");
|
|
18
|
-
this.root = config.root
|
|
64
|
+
this.root = config.root || skillsRoot();
|
|
65
|
+
this.eventsHome = config.eventsHome || process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
19
66
|
this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
67
|
+
ctx.on("session/event", (_session, event) => {
|
|
68
|
+
if (event.type !== "tool/call") return;
|
|
69
|
+
if (!READ_TOOL_KIND[event.data.name]) return;
|
|
70
|
+
const name = skillNameFromToolCall(event.data.arguments);
|
|
71
|
+
if (!name) return;
|
|
72
|
+
this.observeRead(name).catch(() => {});
|
|
73
|
+
});
|
|
20
74
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Read-side telemetry (A2): bump the view counter for an EXISTING record
|
|
77
|
+
* only. Creation-free by design — otherwise an arbitrary read mints a
|
|
78
|
+
* record, and "patched many times, reads zero" (the write-ghost signal)
|
|
79
|
+
* stays computable only while records exist from authorship paths.
|
|
80
|
+
*
|
|
81
|
+
* Observation window (C): the FIRST observed read library-wide opens the
|
|
82
|
+
* window — one `type:'usage'` event is appended to the evolution timeline
|
|
83
|
+
* (best-effort; the usage sidecar stays the truth and never fails the read
|
|
84
|
+
* path). Post-A2 deployments treat `view_count` as trustworthy only after
|
|
85
|
+
* that anchor exists (curator gates churn on `usageObserved()`).
|
|
86
|
+
*/
|
|
87
|
+
observeRead(name) {
|
|
88
|
+
return this.mutate(async (map) => {
|
|
89
|
+
if (!map.has(name)) return;
|
|
90
|
+
const viewsBefore = usageTotals(map).views;
|
|
91
|
+
bumpView(map, name, /* @__PURE__ */ new Date());
|
|
92
|
+
if (viewsBefore === 0) await this.appendUsageWindowEvent(map);
|
|
93
|
+
});
|
|
24
94
|
}
|
|
25
|
-
|
|
26
|
-
|
|
95
|
+
/** Append the observation-window anchor event; best-effort, never fails the observation. */
|
|
96
|
+
async appendUsageWindowEvent(map) {
|
|
97
|
+
try {
|
|
98
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
99
|
+
await appendEvolutionEvent(this.io, eventsFile(this.eventsHome), {
|
|
100
|
+
type: "usage",
|
|
101
|
+
kind: "skill",
|
|
102
|
+
source: "observation",
|
|
103
|
+
note: "observation window opened",
|
|
104
|
+
counts: usageTotals(map),
|
|
105
|
+
window: { opened: at }
|
|
106
|
+
});
|
|
107
|
+
} catch {}
|
|
27
108
|
}
|
|
28
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Serialize read-modify-write cycles so concurrent record calls never lose
|
|
111
|
+
* updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
|
|
112
|
+
* is read from disk inside the lock, so a second process that shares
|
|
113
|
+
* DSH_HOME cannot interleave its own RMW between our read and write.
|
|
114
|
+
*/
|
|
29
115
|
mutate(task) {
|
|
30
|
-
const run = this.chain.then(async () =>
|
|
116
|
+
const run = this.chain.then(async () => {
|
|
117
|
+
let outcome = void 0;
|
|
118
|
+
await mutateUsage(this.root, this.io, async (map) => {
|
|
119
|
+
outcome = await task(map);
|
|
120
|
+
});
|
|
121
|
+
return outcome;
|
|
122
|
+
});
|
|
31
123
|
this.chain = run.then(() => void 0, () => void 0);
|
|
32
124
|
return run;
|
|
33
125
|
}
|
|
34
126
|
async record(name, kind, at = /* @__PURE__ */ new Date()) {
|
|
35
|
-
await this.mutate(
|
|
127
|
+
await this.mutate((map) => {
|
|
36
128
|
if (kind === "use") bumpUse(map, name, at);
|
|
37
129
|
else if (kind === "view") bumpView(map, name, at);
|
|
38
130
|
else bumpPatch(map, name, at);
|
|
39
|
-
await this.flush();
|
|
40
131
|
});
|
|
41
132
|
}
|
|
42
|
-
|
|
43
|
-
|
|
133
|
+
/** Read-only snapshot of the sidecar (no disk write — reading never mutates). */
|
|
134
|
+
async report() {
|
|
135
|
+
const run = this.chain.then(async () => new Map(await loadUsage(this.root, this.io)));
|
|
136
|
+
this.chain = run.then(() => void 0, () => void 0);
|
|
137
|
+
return run;
|
|
44
138
|
}
|
|
45
139
|
async markAgentCreated(name) {
|
|
46
|
-
await this.mutate(
|
|
47
|
-
const { markAgentCreated } = await import("@lmzhen/dsh-evolution-core");
|
|
140
|
+
await this.mutate((map) => {
|
|
48
141
|
markAgentCreated(map, name);
|
|
49
|
-
await this.flush();
|
|
50
142
|
});
|
|
51
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Ensure a usage record exists for `name` without bumping any counter
|
|
146
|
+
* (rc.46 M3-3.3 companion): skill creation is authorship — the record must
|
|
147
|
+
* exist from birth (created_at anchors now) but `patch_count` stays 0 so
|
|
148
|
+
* mutation maturity is not inflated by mere creation.
|
|
149
|
+
*/
|
|
150
|
+
async ensureRecord(name) {
|
|
151
|
+
await this.mutate((map) => {
|
|
152
|
+
getRecord(map, name);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Mark a skill's usage record as archived (delete / curator archive paths).
|
|
157
|
+
* Unlike `record('patch')` this never bumps the patch counter — archiving is
|
|
158
|
+
* a state transition, not a content mutation.
|
|
159
|
+
*/
|
|
160
|
+
async markArchived(name, at = /* @__PURE__ */ new Date()) {
|
|
161
|
+
await this.mutate((map) => {
|
|
162
|
+
const record = map.get(name);
|
|
163
|
+
if (record) {
|
|
164
|
+
record.state = "archived";
|
|
165
|
+
record.archived_at = at.toISOString();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Barrier for external writers: every mutate reads the sidecar fresh from
|
|
171
|
+
* disk (rc.50 P2-2 transact), so a curator direct-write is visible on the
|
|
172
|
+
* next call without a cache flush; this waits for queued work to drain.
|
|
173
|
+
*/
|
|
174
|
+
async invalidate() {
|
|
175
|
+
await this.chain;
|
|
176
|
+
}
|
|
52
177
|
/** Write feedback-derived quality onto the usage sidecar; curator reads it. */
|
|
53
178
|
async setQuality(name, score, warn) {
|
|
54
|
-
await this.mutate(
|
|
179
|
+
await this.mutate((map) => {
|
|
55
180
|
const record = map.get(name);
|
|
56
181
|
if (!record) return;
|
|
57
182
|
record.quality_score = score;
|
|
58
183
|
record.quality_warn = warn;
|
|
59
|
-
await this.flush();
|
|
60
184
|
});
|
|
61
185
|
}
|
|
62
186
|
};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,22 +12,62 @@ declare module '@deepseek-ai/cordis' {
|
|
|
12
12
|
}
|
|
13
13
|
export interface Config {
|
|
14
14
|
root?: string;
|
|
15
|
+
/** Home for the evolution event timeline (`<eventsHome>/evolution/events.json`); defaults to DSH_HOME or ~/.dsh. */
|
|
16
|
+
eventsHome?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare class SkillUsageRegistry extends Service {
|
|
17
19
|
static inject: string[];
|
|
18
20
|
static Config: Schema<Config>;
|
|
19
21
|
readonly root: string;
|
|
22
|
+
private readonly eventsHome;
|
|
20
23
|
private readonly io;
|
|
21
|
-
private usage;
|
|
22
24
|
private chain;
|
|
23
25
|
constructor(ctx: Context, config?: Config);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Read-side telemetry (A2): bump the view counter for an EXISTING record
|
|
28
|
+
* only. Creation-free by design — otherwise an arbitrary read mints a
|
|
29
|
+
* record, and "patched many times, reads zero" (the write-ghost signal)
|
|
30
|
+
* stays computable only while records exist from authorship paths.
|
|
31
|
+
*
|
|
32
|
+
* Observation window (C): the FIRST observed read library-wide opens the
|
|
33
|
+
* window — one `type:'usage'` event is appended to the evolution timeline
|
|
34
|
+
* (best-effort; the usage sidecar stays the truth and never fails the read
|
|
35
|
+
* path). Post-A2 deployments treat `view_count` as trustworthy only after
|
|
36
|
+
* that anchor exists (curator gates churn on `usageObserved()`).
|
|
37
|
+
*/
|
|
38
|
+
private observeRead;
|
|
39
|
+
/** Append the observation-window anchor event; best-effort, never fails the observation. */
|
|
40
|
+
private appendUsageWindowEvent;
|
|
41
|
+
/**
|
|
42
|
+
* Serialize read-modify-write cycles so concurrent record calls never lose
|
|
43
|
+
* updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
|
|
44
|
+
* is read from disk inside the lock, so a second process that shares
|
|
45
|
+
* DSH_HOME cannot interleave its own RMW between our read and write.
|
|
46
|
+
*/
|
|
27
47
|
private mutate;
|
|
28
48
|
record(name: string, kind: 'use' | 'view' | 'patch', at?: Date): Promise<void>;
|
|
49
|
+
/** Read-only snapshot of the sidecar (no disk write — reading never mutates). */
|
|
29
50
|
report(): Promise<UsageMap>;
|
|
30
51
|
markAgentCreated(name: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Ensure a usage record exists for `name` without bumping any counter
|
|
54
|
+
* (rc.46 M3-3.3 companion): skill creation is authorship — the record must
|
|
55
|
+
* exist from birth (created_at anchors now) but `patch_count` stays 0 so
|
|
56
|
+
* mutation maturity is not inflated by mere creation.
|
|
57
|
+
*/
|
|
58
|
+
ensureRecord(name: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Mark a skill's usage record as archived (delete / curator archive paths).
|
|
61
|
+
* Unlike `record('patch')` this never bumps the patch counter — archiving is
|
|
62
|
+
* a state transition, not a content mutation.
|
|
63
|
+
*/
|
|
64
|
+
markArchived(name: string, at?: Date): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Barrier for external writers: every mutate reads the sidecar fresh from
|
|
67
|
+
* disk (rc.50 P2-2 transact), so a curator direct-write is visible on the
|
|
68
|
+
* next call without a cache flush; this waits for queued work to drain.
|
|
69
|
+
*/
|
|
70
|
+
invalidate(): Promise<void>;
|
|
31
71
|
/** Write feedback-derived quality onto the usage sidecar; curator reads it. */
|
|
32
72
|
setQuality(name: string, score: number, warn: boolean): Promise<void>;
|
|
33
73
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-skill-usage",
|
|
3
3
|
"description": "Skill usage telemetry service (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,16 +33,18 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
41
|
-
"@
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
42
|
+
"@lmzhen/dsh-evolution-io": "^0.2.0-rc.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
45
|
-
"@
|
|
46
|
-
"@lmzhen/dsh-evolution-
|
|
45
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
47
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1",
|
|
48
|
+
"@lmzhen/dsh-evolution-io": "^0.2.0-rc.1"
|
|
47
49
|
}
|
|
48
50
|
}
|