@lmzhen/dsh-skill-usage 0.3.17 → 0.3.18

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/index.js CHANGED
@@ -2,7 +2,7 @@ import { Service } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { appendEvolutionEvent, bumpPatch, bumpUse, bumpView, eventsFile, evolutionIoAdapter, getRecord, loadUsage, markAgentCreated, mutateUsage, skillsRoot } from "@lmzhen/dsh-evolution-core";
5
+ import { appendEvolutionEvent, bumpPatch, bumpUse, bumpView, eventsFile, evolutionIoAdapter, getRecord, loadUsage, markAgentCreated, mutateUsage, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
6
6
  //#region lib/types/index.js
7
7
  /**
8
8
  * Skill usage telemetry service for the evolution family.
@@ -61,13 +61,14 @@ var SkillUsageRegistry = class extends Service {
61
61
  chain = Promise.resolve();
62
62
  constructor(ctx, config = {}) {
63
63
  super(ctx, "skillUsage");
64
- this.root = config.root || skillsRoot();
64
+ this.root = resolveSkillsRoot(config);
65
65
  this.eventsHome = config.eventsHome || process.env.DSH_HOME || join(homedir(), ".dsh");
66
66
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
67
67
  ctx.on("session/event", (_session, event) => {
68
68
  if (event.type !== "tool/call") return;
69
- if (!READ_TOOL_KIND[event.data.name]) return;
70
- const name = skillNameFromToolCall(event.data.arguments);
69
+ const data = event.data;
70
+ if (!(typeof data?.name === "string" ? READ_TOOL_KIND[data.name] : void 0)) return;
71
+ const name = skillNameFromToolCall(data?.arguments);
71
72
  if (!name) return;
72
73
  this.observeRead(name).catch(() => {});
73
74
  });
@@ -92,7 +93,15 @@ var SkillUsageRegistry = class extends Service {
92
93
  if (viewsBefore === 0) await this.appendUsageWindowEvent(map);
93
94
  });
94
95
  }
95
- /** Append the observation-window anchor event; best-effort, never fails the observation. */
96
+ /**
97
+ * Append the observation-window anchor event; best-effort, never fails the
98
+ * observation.
99
+ *
100
+ * Lock-order contract (E-66): this acquires the evolution-events lock while
101
+ * the caller holds the usage lock (usage → events one-way nesting — see
102
+ * `mutate`). Never take the usage lock from inside a block that holds the
103
+ * events lock; that reverse nesting is a deadlock.
104
+ */
96
105
  async appendUsageWindowEvent(map) {
97
106
  try {
98
107
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -104,13 +113,21 @@ var SkillUsageRegistry = class extends Service {
104
113
  counts: usageTotals(map),
105
114
  window: { opened: at }
106
115
  });
107
- } catch {}
116
+ } catch (error) {
117
+ this.ctx.logger.warn(`dsh-skill-usage: failed to append observation-window anchor: ${error instanceof Error ? error.message : String(error)}`);
118
+ }
108
119
  }
109
120
  /**
110
121
  * Serialize read-modify-write cycles so concurrent record calls never lose
111
122
  * updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
112
123
  * is read from disk inside the lock, so a second process that shares
113
124
  * DSH_HOME cannot interleave its own RMW between our read and write.
125
+ *
126
+ * Lock-order contract (E-66): this holds the usage (`.usage.json`) lock. A
127
+ * usage-locked `task` MAY acquire the evolution-events lock one-way
128
+ * (usage → events, e.g. via `appendUsageWindowEvent`); it must NEVER acquire
129
+ * it in the reverse direction. Two writers taking the two locks in opposite
130
+ * order would deadlock, so the one-way order is a binding invariant.
114
131
  */
115
132
  mutate(task) {
116
133
  const run = this.chain.then(async () => {
@@ -153,6 +170,19 @@ var SkillUsageRegistry = class extends Service {
153
170
  });
154
171
  }
155
172
  /**
173
+ * 0.3.18 (E-70): create-path telemetry in ONE atomic RMW. The former
174
+ * prepare→markAgentCreated pair ran two full transacts (doubled lock
175
+ * traffic) and left a window where `created_by=null` was on disk between
176
+ * them. `agentCreated` picks the authorship marking at the same commit
177
+ * point where the record is created.
178
+ */
179
+ async ensureRecordCreated(name, agentCreated) {
180
+ await this.mutate((map) => {
181
+ if (agentCreated) markAgentCreated(map, name);
182
+ else getRecord(map, name);
183
+ });
184
+ }
185
+ /**
156
186
  * Mark a skill's usage record as archived (delete / curator archive paths).
157
187
  * Unlike `record('patch')` this never bumps the patch counter — archiving is
158
188
  * a state transition, not a content mutation.
@@ -36,13 +36,27 @@ export declare class SkillUsageRegistry extends Service {
36
36
  * that anchor exists (curator gates churn on `usageObserved()`).
37
37
  */
38
38
  private observeRead;
39
- /** Append the observation-window anchor event; best-effort, never fails the observation. */
39
+ /**
40
+ * Append the observation-window anchor event; best-effort, never fails the
41
+ * observation.
42
+ *
43
+ * Lock-order contract (E-66): this acquires the evolution-events lock while
44
+ * the caller holds the usage lock (usage → events one-way nesting — see
45
+ * `mutate`). Never take the usage lock from inside a block that holds the
46
+ * events lock; that reverse nesting is a deadlock.
47
+ */
40
48
  private appendUsageWindowEvent;
41
49
  /**
42
50
  * Serialize read-modify-write cycles so concurrent record calls never lose
43
51
  * updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
44
52
  * is read from disk inside the lock, so a second process that shares
45
53
  * DSH_HOME cannot interleave its own RMW between our read and write.
54
+ *
55
+ * Lock-order contract (E-66): this holds the usage (`.usage.json`) lock. A
56
+ * usage-locked `task` MAY acquire the evolution-events lock one-way
57
+ * (usage → events, e.g. via `appendUsageWindowEvent`); it must NEVER acquire
58
+ * it in the reverse direction. Two writers taking the two locks in opposite
59
+ * order would deadlock, so the one-way order is a binding invariant.
46
60
  */
47
61
  private mutate;
48
62
  record(name: string, kind: 'use' | 'view' | 'patch', at?: Date): Promise<void>;
@@ -56,6 +70,14 @@ export declare class SkillUsageRegistry extends Service {
56
70
  * mutation maturity is not inflated by mere creation.
57
71
  */
58
72
  ensureRecord(name: string): Promise<void>;
73
+ /**
74
+ * 0.3.18 (E-70): create-path telemetry in ONE atomic RMW. The former
75
+ * prepare→markAgentCreated pair ran two full transacts (doubled lock
76
+ * traffic) and left a window where `created_by=null` was on disk between
77
+ * them. `agentCreated` picks the authorship marking at the same commit
78
+ * point where the record is created.
79
+ */
80
+ ensureRecordCreated(name: string, agentCreated: boolean): Promise<void>;
59
81
  /**
60
82
  * Mark a skill's usage record as archived (delete / curator archive paths).
61
83
  * Unlike `record('patch')` this never bumps the patch counter — archiving is
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.3.17",
4
+ "version": "0.3.18",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,18 +33,18 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.3.17"
36
+ "@lmzhen/dsh-evolution-core": "^0.3.18"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
40
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
41
41
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
42
- "@lmzhen/dsh-evolution-io": "^0.3.17"
42
+ "@lmzhen/dsh-evolution-io": "^0.3.18"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
46
46
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
47
- "@lmzhen/dsh-evolution-core": "^0.3.17",
48
- "@lmzhen/dsh-evolution-io": "^0.3.17"
47
+ "@lmzhen/dsh-evolution-core": "^0.3.18",
48
+ "@lmzhen/dsh-evolution-io": "^0.3.18"
49
49
  }
50
50
  }