@x-otto/cli 0.0.1-alpha.38 → 0.0.1-alpha.39

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.
Files changed (29) hide show
  1. package/dist/build-id-C0TV2Bnh.js +3 -0
  2. package/dist/build-id-C0TV2Bnh.js.map +1 -0
  3. package/dist/{command-dispatcher-D_QIlalH.js → command-dispatcher-Dl9xnGot.js} +2 -2
  4. package/dist/{command-dispatcher-D_QIlalH.js.map → command-dispatcher-Dl9xnGot.js.map} +1 -1
  5. package/dist/create-host-app-BXQ8bU-B.js.map +1 -1
  6. package/dist/{feedback-DmBlIeGZ.js → feedback-B-rvGLBX.js} +2 -2
  7. package/dist/{feedback-DmBlIeGZ.js.map → feedback-B-rvGLBX.js.map} +1 -1
  8. package/dist/feedback-CoBDsaba.js +2 -0
  9. package/dist/index.js +3 -3
  10. package/dist/{parse-cli-Cf9nKV3P.js → parse-cli-TKDV62MY.js} +2 -2
  11. package/dist/{parse-cli-Cf9nKV3P.js.map → parse-cli-TKDV62MY.js.map} +1 -1
  12. package/dist/{version-D-9A3yFu.js → version-DjQjYPTu.js} +2 -2
  13. package/dist/{version-D-9A3yFu.js.map → version-DjQjYPTu.js.map} +1 -1
  14. package/package.json +13 -13
  15. package/dist/build-id-BUDK-CNM.js +0 -3
  16. package/dist/build-id-BUDK-CNM.js.map +0 -1
  17. package/dist/bundled-extensions/plugin-skill-inductor/otto-plugin.json +0 -8
  18. package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/meta.json +0 -1
  19. package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/plugin.cjs +0 -565
  20. package/dist/bundled-extensions/plugin-skill-inductor/plugin.ts +0 -77
  21. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-detector.ts +0 -210
  22. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-digest.ts +0 -146
  23. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-store.ts +0 -87
  24. package/dist/bundled-extensions/plugin-skill-inductor/src/episode-store.ts +0 -122
  25. package/dist/bundled-extensions/plugin-skill-inductor/src/episode.ts +0 -125
  26. package/dist/bundled-extensions/plugin-skill-inductor/src/extraction-prompt.ts +0 -97
  27. package/dist/bundled-extensions/plugin-skill-inductor/src/inductor-service.ts +0 -183
  28. package/dist/bundled-extensions/plugin-skill-inductor/src/similarity.ts +0 -58
  29. package/dist/feedback-QYZ1E8Kc.js +0 -2
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{r as e}from"./build-id-BUDK-CNM.js";import{createRequire as t}from"node:module";const n=t(import.meta.url);function r(){return`${n(`../package.json`).version??`0.0.0`} (build ${e})`}export{r as t};
3
- //# sourceMappingURL=version-D-9A3yFu.js.map
2
+ import{r as e}from"./build-id-C0TV2Bnh.js";import{createRequire as t}from"node:module";const n=t(import.meta.url);function r(){return`${n(`../package.json`).version??`0.0.0`} (build ${e})`}export{r as t};
3
+ //# sourceMappingURL=version-DjQjYPTu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-D-9A3yFu.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["import { createRequire } from 'node:module'\n\nimport { BUILD_ID } from './build-id'\n\nconst require = createRequire(import.meta.url)\n\n/**\n * 版本号 + build ID。\n * 格式:`0.1.0 (build 2072)`。build ID = git rev-list --count HEAD,随每次构建自增。\n */\nexport function getVersion(): string {\n const pkg = require('../package.json') as { version?: string }\n const ver = pkg.version ?? '0.0.0'\n return `${ver} (build ${BUILD_ID})`\n}\n"],"mappings":";uFAIA,MAAM,EAAU,EAAc,OAAO,KAAK,IAAI,CAM9C,SAAgB,GAAqB,CAGnC,MAAO,GAFK,EAAQ,kBAAkB,CACtB,SAAW,QACb,UAAU,EAAS"}
1
+ {"version":3,"file":"version-DjQjYPTu.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["import { createRequire } from 'node:module'\n\nimport { BUILD_ID } from './build-id'\n\nconst require = createRequire(import.meta.url)\n\n/**\n * 版本号 + build ID。\n * 格式:`0.1.0 (build 2072)`。build ID = git rev-list --count HEAD,随每次构建自增。\n */\nexport function getVersion(): string {\n const pkg = require('../package.json') as { version?: string }\n const ver = pkg.version ?? '0.0.0'\n return `${ver} (build ${BUILD_ID})`\n}\n"],"mappings":";uFAIA,MAAM,EAAU,EAAc,OAAO,KAAK,IAAI,CAM9C,SAAgB,GAAqB,CAGnC,MAAO,GAFK,EAAQ,kBAAkB,CACtB,SAAW,QACb,UAAU,EAAS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-otto/cli",
3
- "version": "0.0.1-alpha.38",
3
+ "version": "0.0.1-alpha.39",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,24 +20,24 @@
20
20
  "react": "^19.2.4",
21
21
  "@x-otto/agent": "0.0.1-alpha.2",
22
22
  "@x-otto/ai": "0.0.1-alpha.2",
23
- "@x-otto/env": "0.1.0-alpha.6",
24
- "@x-otto/coding": "0.0.1-alpha.15",
25
- "@x-otto/hooks": "0.0.1-alpha.2",
26
- "@x-otto/schedule": "0.0.1-alpha.2",
27
23
  "@x-otto/mcp": "0.0.1-alpha.2",
28
- "@x-otto/interchange": "0.1.0-alpha.3",
24
+ "@x-otto/schedule": "0.0.1-alpha.2",
25
+ "@x-otto/coding": "0.0.1-alpha.15",
26
+ "@x-otto/env": "0.1.0-alpha.6",
29
27
  "@x-otto/memory": "0.0.1-alpha.2",
30
28
  "@x-otto/persistence": "0.0.1-alpha.3",
31
- "@x-otto/runtime": "0.0.1-alpha.5",
32
- "@x-otto/service": "0.0.1-alpha.2",
29
+ "@x-otto/interchange": "0.1.0-alpha.3",
30
+ "@x-otto/remote-persistence-server": "0.0.1-alpha.3",
33
31
  "@x-otto/session": "0.0.1-alpha.4",
32
+ "@x-otto/service": "0.0.1-alpha.2",
33
+ "@x-otto/runtime": "0.0.1-alpha.5",
34
34
  "@x-otto/session-contract": "0.0.1-alpha.2",
35
- "@x-otto/remote-persistence-server": "0.0.1-alpha.3",
36
- "@x-otto/shared": "0.1.0-alpha.6",
37
- "@x-otto/setting": "0.0.1-alpha.3",
38
35
  "@x-otto/trace-view": "0.0.1-alpha.2",
39
- "@x-otto/tools": "0.0.1-alpha.2",
40
- "@x-otto/tui": "0.0.1-alpha.14"
36
+ "@x-otto/setting": "0.0.1-alpha.3",
37
+ "@x-otto/shared": "0.1.0-alpha.6",
38
+ "@x-otto/tui": "0.0.1-alpha.14",
39
+ "@x-otto/hooks": "0.0.1-alpha.2",
40
+ "@x-otto/tools": "0.0.1-alpha.2"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- const e=4856,t=`HEAD`,n=`2800ac85a`;export{n,e as r,t};
3
- //# sourceMappingURL=build-id-BUDK-CNM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-id-BUDK-CNM.js","names":[],"sources":["../src/build-id.ts"],"sourcesContent":["// auto-generated by scripts/gen-build-id.mjs — DO NOT EDIT\nexport const BUILD_ID = 4856\nexport const BUILD_BRANCH = 'HEAD'\nexport const BUILD_HASH = '2800ac85a'\n"],"mappings":";AACA,MAAa,EAAW,KACX,EAAe,OACf,EAAa"}
@@ -1,8 +0,0 @@
1
- {
2
- "id": "plugin-skill-inductor",
3
- "name": "Skill Inductor",
4
- "version": "0.1.0",
5
- "description": "Observes completed task trajectories and distills them into structured episodes — the data source for auto-induced skills (RFC-318 skill loop).",
6
- "capabilities": ["service", "storage", "session.read", "llm.complete", "user.notify"],
7
- "scripts": { "postinstall": "otto extension build" }
8
- }
@@ -1 +0,0 @@
1
- {"apiVersion":1,"compilerVersion":"0.1.0-alpha.6","sourceHash":"5b28454cfded236868d7031600579b7aa59f72785a003a28034b3cb81c5b70ae"}
@@ -1,565 +0,0 @@
1
- const __otto_plugin_import_meta_url = require("url").pathToFileURL(__filename).href;
2
- "use strict";
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // ../../extensions/plugin-skill-inductor/plugin.ts
22
- var plugin_exports = {};
23
- __export(plugin_exports, {
24
- default: () => plugin_default
25
- });
26
- module.exports = __toCommonJS(plugin_exports);
27
-
28
- // otto-sdk-shim:otto-plugin-sdk
29
- var definePlugin = (f) => f;
30
-
31
- // ../../extensions/plugin-skill-inductor/src/episode.ts
32
- var MAX_SIGNATURE_CHARS = 200;
33
- var MAX_INTENT_CHARS = 120;
34
- var MAX_STEPS = 20;
35
- var MAX_TAGS = 5;
36
- var OUTCOMES = /* @__PURE__ */ new Set(["success", "partial", "failure"]);
37
- function clamp(text, max) {
38
- return text.length > max ? text.slice(0, max) : text;
39
- }
40
- function normalizeSteps(raw) {
41
- if (!Array.isArray(raw)) return [];
42
- const steps = [];
43
- for (const item of raw) {
44
- if (steps.length >= MAX_STEPS) break;
45
- const s = item;
46
- const tool = typeof s?.tool === "string" ? s.tool.trim() : "";
47
- if (!tool) continue;
48
- const intent = typeof s?.intent === "string" ? s.intent.trim() : "";
49
- steps.push({ tool: clamp(tool, MAX_INTENT_CHARS), intent: clamp(intent, MAX_INTENT_CHARS) });
50
- }
51
- return steps;
52
- }
53
- function normalizeTags(raw) {
54
- if (!Array.isArray(raw)) return [];
55
- const tags = [];
56
- for (const item of raw) {
57
- if (tags.length >= MAX_TAGS) break;
58
- if (typeof item !== "string") continue;
59
- const tag = item.trim().toLowerCase();
60
- if (tag && !tags.includes(tag)) tags.push(tag);
61
- }
62
- return tags;
63
- }
64
- function parseEpisode(raw, sessionId, now) {
65
- let data;
66
- try {
67
- data = JSON.parse(extractJsonBlock(raw));
68
- } catch {
69
- return null;
70
- }
71
- if (!data || typeof data !== "object" || Array.isArray(data)) return null;
72
- const obj = data;
73
- const taskSignature = typeof obj["taskSignature"] === "string" ? obj["taskSignature"].trim() : "";
74
- if (!taskSignature) return null;
75
- const steps = normalizeSteps(obj["steps"]);
76
- if (steps.length === 0) return null;
77
- const outcomeRaw = typeof obj["outcome"] === "string" ? obj["outcome"] : "";
78
- const outcome = OUTCOMES.has(outcomeRaw) ? outcomeRaw : "partial";
79
- return {
80
- taskSignature: clamp(taskSignature, MAX_SIGNATURE_CHARS),
81
- tags: normalizeTags(obj["tags"]),
82
- steps,
83
- outcome,
84
- sessionId,
85
- at: now
86
- };
87
- }
88
- function extractJsonBlock(raw) {
89
- const trimmed = raw.trim();
90
- const start = trimmed.indexOf("{");
91
- const end = trimmed.lastIndexOf("}");
92
- if (start === -1 || end === -1 || end <= start) return trimmed;
93
- return trimmed.slice(start, end + 1);
94
- }
95
-
96
- // ../../extensions/plugin-skill-inductor/src/extraction-prompt.ts
97
- var EPISODE_EXTRACTION_PROMPT = `You are a trajectory analyst for an AI coding assistant. Read the session transcript below and extract the reusable SOLUTION PATH \u2014 the ordered tool steps \u2014 of the task that was carried out, so that similar future tasks can follow the same route.
98
-
99
- ## STEP 1 \u2014 Decide whether to extract anything at all
100
-
101
- Output exactly \`null\` (nothing else, no JSON) if ANY of these is true:
102
-
103
- 1. No task was carried out \u2014 the session is chat, Q&A, explanation, or planning only.
104
- 2. Fewer than 2 real tool calls appear in the transcript.
105
- 3. The work is still in progress \u2014 no success, failure, or abandonment is visible yet.
106
- 4. The path is trivial: a single lookup answered directly (e.g. read one file \u2192 answer), with no decision-making, no iteration, no repair.
107
-
108
- Be strict here. Most sessions deserve \`null\`. A wrong \`null\` costs nothing; a fabricated episode pollutes the skill library permanently.
109
-
110
- ## STEP 2 \u2014 Only if none of the above applies, output ONE JSON object
111
-
112
- {
113
- "taskSignature": string, // The TASK TYPE, with every specific identifier generalized away.
114
- // Replace package names, file paths, function names, variable names,
115
- // and product names with their CATEGORY or ROLE.
116
- // It must describe a class of tasks, not this one instance.
117
- "tags": string[], // 1-5 lowercase keywords for retrieval
118
- "steps": [ // The real tool sequence, in order
119
- { "tool": string, // Tool name EXACTLY as it appears in the transcript \u2014 never invent one
120
- "intent": string } // One short phrase: what this step accomplished
121
- ],
122
- "outcome": "success" | "partial" | "failure"
123
- }
124
-
125
- Rules for steps:
126
- - Every step MUST correspond to a real tool call visible in the transcript. Do NOT invent, merge unrelated calls, or idealize the path into what "should" have been done.
127
- - Collapse trivial repetition: five consecutive reads of different files become one "read" step describing the group.
128
- - Drop abandoned detours that had no effect on the outcome.
129
-
130
- ## Examples
131
-
132
- ### Example A \u2014 a real task (extract)
133
-
134
- Transcript: user asks to add a rate limit to an API endpoint. Assistant greps for the middleware registry, reads the existing throttle helper, edits the route to apply it, runs the test suite, sees one failure, fixes the config, reruns \u2014 green.
135
-
136
- Output:
137
- {
138
- "taskSignature": "apply an existing cross-cutting middleware to a specific API route and get the suite green",
139
- "tags": ["api", "middleware", "config"],
140
- "steps": [
141
- { "tool": "grep", "intent": "locate where middleware is registered" },
142
- { "tool": "read", "intent": "study the existing helper's interface" },
143
- { "tool": "edit", "intent": "apply the middleware to the target route" },
144
- { "tool": "bash", "intent": "run the test suite, one failure surfaced" },
145
- { "tool": "edit", "intent": "correct the configuration value" },
146
- { "tool": "bash", "intent": "rerun \u2014 all green" }
147
- ],
148
- "outcome": "success"
149
- }
150
-
151
- Note how the signature says "an existing cross-cutting middleware" and "a specific API route" instead of naming the rate limiter or the route \u2014 that is what makes it match future tasks of the same shape.
152
-
153
- ### Example B \u2014 not a task (do not extract)
154
-
155
- Transcript: user asks how the caching layer works. Assistant reads two files and explains the design. No changes made.
156
-
157
- Output:
158
- null
159
-
160
- ## Output format
161
-
162
- Output ONLY the JSON object, or the bare word \`null\`. No prose, no code fences, no explanation.`;
163
- function buildEpisodePrompt(messages) {
164
- const lines = messages.map((m) => {
165
- const tag = m.toolName ? `${m.role}(${m.toolName})` : m.role;
166
- return `<${tag}>
167
- ${m.text}
168
- </${tag}>`;
169
- });
170
- return `${EPISODE_EXTRACTION_PROMPT}
171
-
172
- <transcript>
173
- ${lines.join("\n")}
174
- </transcript>`;
175
- }
176
-
177
- // ../../extensions/plugin-skill-inductor/src/episode-store.ts
178
- var EPISODE_RECORD_ID = "episodes";
179
- var MAX_EPISODES = 200;
180
- var MAX_CURSORS = 500;
181
- function emptyRecord() {
182
- return { items: [], cursor: {} };
183
- }
184
- async function loadRecord(storage) {
185
- let raw;
186
- try {
187
- raw = await storage.load(EPISODE_RECORD_ID);
188
- } catch {
189
- return emptyRecord();
190
- }
191
- if (!raw) return emptyRecord();
192
- const items = Array.isArray(raw["items"]) ? raw["items"] : [];
193
- const cursorRaw = raw["cursor"];
194
- const cursor = {};
195
- if (cursorRaw && typeof cursorRaw === "object" && !Array.isArray(cursorRaw)) {
196
- for (const [k, v] of Object.entries(cursorRaw)) {
197
- if (typeof v === "number" && Number.isFinite(v) && v >= 0) cursor[k] = v;
198
- }
199
- }
200
- return { items, cursor };
201
- }
202
- function appendEpisode(record, episode) {
203
- const items = [...record.items, episode];
204
- const overflow = items.length - MAX_EPISODES;
205
- return {
206
- items: overflow > 0 ? items.slice(overflow) : items,
207
- cursor: record.cursor
208
- };
209
- }
210
- function advanceCursor(record, sessionId, nextIndex) {
211
- const cursor = { ...record.cursor };
212
- delete cursor[sessionId];
213
- cursor[sessionId] = nextIndex;
214
- const keys = Object.keys(cursor);
215
- if (keys.length > MAX_CURSORS) {
216
- for (const stale of keys.slice(0, keys.length - MAX_CURSORS)) delete cursor[stale];
217
- }
218
- return { items: record.items, cursor };
219
- }
220
- async function saveRecord(storage, record) {
221
- try {
222
- await storage.save(EPISODE_RECORD_ID, { items: record.items, cursor: record.cursor });
223
- return true;
224
- } catch {
225
- return false;
226
- }
227
- }
228
-
229
- // ../../extensions/plugin-skill-inductor/src/candidate-detector.ts
230
- var import_node_crypto = require("node:crypto");
231
-
232
- // ../../extensions/plugin-skill-inductor/src/similarity.ts
233
- var SIGNATURE_WEIGHT = 0.8;
234
- var TAG_WEIGHT = 0.2;
235
- function toWordSet(text) {
236
- return new Set(
237
- text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 2)
238
- );
239
- }
240
- function jaccard(a, b) {
241
- if (a.size === 0 || b.size === 0) return 0;
242
- let intersection = 0;
243
- for (const w of a) {
244
- if (b.has(w)) intersection++;
245
- }
246
- return intersection / (a.size + b.size - intersection);
247
- }
248
- function episodeSimilarity(a, b) {
249
- const signature = jaccard(toWordSet(a.taskSignature), toWordSet(b.taskSignature));
250
- const tags = jaccard(new Set(a.tags), new Set(b.tags));
251
- return SIGNATURE_WEIGHT * signature + TAG_WEIGHT * tags;
252
- }
253
-
254
- // ../../extensions/plugin-skill-inductor/src/candidate-detector.ts
255
- var DETECT_WINDOW = 50;
256
- var SIMILARITY_THRESHOLD = 0.25;
257
- var MIN_CLUSTER_SIZE = 3;
258
- function clusterIdOf(members, minClusterSize) {
259
- const seed = [...members].sort((a, b) => a.at - b.at).slice(0, minClusterSize).map((e) => e.sessionId).join("|");
260
- return `cand_${(0, import_node_crypto.createHash)("sha256").update(seed).digest("hex").slice(0, 12)}`;
261
- }
262
- function detectCandidates(episodes, options = {}) {
263
- const window = options.window ?? DETECT_WINDOW;
264
- const threshold = options.threshold ?? SIMILARITY_THRESHOLD;
265
- const minClusterSize = options.minClusterSize ?? MIN_CLUSTER_SIZE;
266
- const recent = [...episodes].filter((e) => isUsable(e) && e.outcome === "success").sort((a, b) => a.at - b.at).slice(-window);
267
- return groupByConnectivity(recent, threshold).filter((c) => c.length >= minClusterSize).map((members) => toCandidate(members, minClusterSize));
268
- }
269
- function isUsable(e) {
270
- return typeof e?.at === "number" && Number.isFinite(e.at) && typeof e.taskSignature === "string" && e.taskSignature.trim().length > 0 && typeof e.sessionId === "string" && Array.isArray(e.tags);
271
- }
272
- function groupByConnectivity(episodes, threshold) {
273
- const parent = episodes.map((_, i) => i);
274
- const find = (i) => {
275
- let root = i;
276
- while (parent[root] !== root) root = parent[root];
277
- let cur = i;
278
- while (parent[cur] !== root) {
279
- const next = parent[cur];
280
- parent[cur] = root;
281
- cur = next;
282
- }
283
- return root;
284
- };
285
- const union = (a, b) => {
286
- const ra = find(a);
287
- const rb = find(b);
288
- if (ra !== rb) parent[Math.max(ra, rb)] = Math.min(ra, rb);
289
- };
290
- for (let i = 0; i < episodes.length; i++) {
291
- for (let j = i + 1; j < episodes.length; j++) {
292
- const a = episodes[i];
293
- const b = episodes[j];
294
- if (episodeSimilarity(a, b) >= threshold) union(i, j);
295
- }
296
- }
297
- const groups = /* @__PURE__ */ new Map();
298
- for (let i = 0; i < episodes.length; i++) {
299
- const root = find(i);
300
- const bucket = groups.get(root);
301
- if (bucket) bucket.push(episodes[i]);
302
- else groups.set(root, [episodes[i]]);
303
- }
304
- return [...groups.values()];
305
- }
306
- function toCandidate(members, minClusterSize) {
307
- const byTime = [...members].sort((a, b) => a.at - b.at);
308
- const representative = [...members].sort(
309
- (a, b) => b.taskSignature.length - a.taskSignature.length
310
- )[0];
311
- const tags = [];
312
- for (const m of byTime) {
313
- for (const t of m.tags) if (!tags.includes(t)) tags.push(t);
314
- }
315
- return {
316
- id: clusterIdOf(members, minClusterSize),
317
- taskSignature: representative.taskSignature,
318
- tags,
319
- sessionIds: [...new Set(byTime.map((m) => m.sessionId))],
320
- size: members.length,
321
- firstAt: byTime[0].at,
322
- lastAt: byTime[byTime.length - 1].at
323
- };
324
- }
325
-
326
- // ../../extensions/plugin-skill-inductor/src/candidate-store.ts
327
- var CANDIDATE_RECORD_ID = "candidates";
328
- var MAX_CANDIDATES = 50;
329
- async function loadCandidates(storage) {
330
- let raw;
331
- try {
332
- raw = await storage.load(CANDIDATE_RECORD_ID);
333
- } catch {
334
- return { items: [] };
335
- }
336
- if (!raw) return { items: [] };
337
- const items = Array.isArray(raw["items"]) ? raw["items"] : [];
338
- return { items };
339
- }
340
- function mergeCandidates(existing, detected) {
341
- const preservedFieldsById = /* @__PURE__ */ new Map();
342
- for (const c of existing) {
343
- const preserved = {};
344
- if (typeof c.dismissedAt === "number") preserved.dismissedAt = c.dismissedAt;
345
- if (typeof c.appliedAt === "number") preserved.appliedAt = c.appliedAt;
346
- if (typeof c.usageCount === "number") preserved.usageCount = c.usageCount;
347
- if (Object.keys(preserved).length > 0) preservedFieldsById.set(c.id, preserved);
348
- }
349
- const merged = [];
350
- const detectedIds = /* @__PURE__ */ new Set();
351
- for (const c of detected) {
352
- detectedIds.add(c.id);
353
- const preserved = preservedFieldsById.get(c.id);
354
- merged.push(preserved ? { ...c, ...preserved } : c);
355
- }
356
- for (const c of existing) {
357
- if (!detectedIds.has(c.id)) merged.push(c);
358
- }
359
- const overflow = merged.length - MAX_CANDIDATES;
360
- return overflow > 0 ? merged.slice(overflow) : merged;
361
- }
362
- async function saveCandidates(storage, record) {
363
- try {
364
- await storage.save(CANDIDATE_RECORD_ID, { items: record.items });
365
- return true;
366
- } catch {
367
- return false;
368
- }
369
- }
370
-
371
- // ../../extensions/plugin-skill-inductor/src/inductor-service.ts
372
- var MIN_MESSAGES_FOR_EXTRACTION = 6;
373
- var MIN_MESSAGES_FLOOR = 2;
374
- function resolveMinMessages(fromHost) {
375
- if (typeof fromHost !== "number" || !Number.isFinite(fromHost)) {
376
- return MIN_MESSAGES_FOR_EXTRACTION;
377
- }
378
- return Math.max(MIN_MESSAGES_FLOOR, Math.floor(fromHost));
379
- }
380
- var EXTRACTION_MAX_TOKENS = 1024;
381
- var SkillInductor = class {
382
- constructor(deps) {
383
- this.deps = deps;
384
- }
385
- deps;
386
- /** 正在处理中的会话——同 session 的并发 notify 直接丢弃(下次 notify 会补上)。 */
387
- inFlight = /* @__PURE__ */ new Set();
388
- get now() {
389
- return this.deps.now ? this.deps.now() : Date.now();
390
- }
391
- async onSessionIdle(sessionId, options) {
392
- const { host, storage, logger } = this.deps;
393
- if (!host?.readTranscript || !host.complete || !storage) {
394
- logger.warn(
395
- "skill-inductor idle: required capabilities missing (needs session.read + llm.complete + storage), skipping"
396
- );
397
- return "missing-capability";
398
- }
399
- if (this.inFlight.has(sessionId)) return "in-flight";
400
- this.inFlight.add(sessionId);
401
- try {
402
- return await this.run(
403
- sessionId,
404
- host.readTranscript,
405
- host.complete,
406
- storage,
407
- resolveMinMessages(options?.minMessages)
408
- );
409
- } catch (err) {
410
- logger.warn(`skill-inductor idle failed: ${err instanceof Error ? err.message : String(err)}`);
411
- return "error";
412
- } finally {
413
- this.inFlight.delete(sessionId);
414
- }
415
- }
416
- async run(sessionId, readTranscript, complete, storage, minMessages) {
417
- const record = await loadRecord(storage);
418
- const sinceIndex = record.cursor[sessionId] ?? 0;
419
- const { messages, nextIndex } = await readTranscript({ sessionId, sinceIndex });
420
- if (messages.length === 0) {
421
- if (nextIndex !== sinceIndex) await saveRecord(storage, advanceCursor(record, sessionId, nextIndex));
422
- return "no-new-messages";
423
- }
424
- if (messages.length < minMessages) {
425
- return "too-short";
426
- }
427
- const raw = await complete({
428
- prompt: buildEpisodePrompt(messages),
429
- maxTokens: EXTRACTION_MAX_TOKENS
430
- });
431
- const episode = parseEpisode(raw, sessionId, this.now);
432
- const advanced = advanceCursor(record, sessionId, nextIndex);
433
- if (!episode) {
434
- await saveRecord(storage, advanced);
435
- return "no-episode";
436
- }
437
- const withEpisode = appendEpisode(advanced, episode);
438
- await saveRecord(storage, withEpisode);
439
- this.deps.logger.info(`skill-inductor captured episode: ${episode.taskSignature}`);
440
- await this.refreshCandidates(storage, withEpisode);
441
- return "saved";
442
- }
443
- /**
444
- * 重算候选并落盘(RFC-318 D-M3-5)。
445
- *
446
- * **失败静默**:这是 episode 抽取的下游附加动作,episode 已经安全落盘;检测出问题不该
447
- * 反过来把主链路标记成失败(R4)。
448
- */
449
- async refreshCandidates(storage, record) {
450
- try {
451
- const detected = detectCandidates(record.items);
452
- const existing = await loadCandidates(storage);
453
- const items = mergeCandidates(existing.items, detected);
454
- await saveCandidates(storage, { items });
455
- } catch (err) {
456
- this.deps.logger.warn(
457
- `skill-inductor candidate detection failed: ${err instanceof Error ? err.message : String(err)}`
458
- );
459
- }
460
- }
461
- };
462
-
463
- // ../../extensions/plugin-skill-inductor/src/candidate-digest.ts
464
- var DIGEST_STATE_ID = "candidate-digest-state";
465
- var DIGEST_MIN_CLUSTER_SIZE = 3;
466
- var DIGEST_MAX_LISTED = 3;
467
- async function loadDigestState(storage) {
468
- try {
469
- const raw = await storage.load(DIGEST_STATE_ID);
470
- if (!raw) return { reportedIds: [] };
471
- const reportedIds = Array.isArray(raw["reportedIds"]) ? raw["reportedIds"] : [];
472
- const lastOccurrenceId = typeof raw["lastOccurrenceId"] === "string" ? raw["lastOccurrenceId"] : void 0;
473
- return lastOccurrenceId ? { reportedIds, lastOccurrenceId } : { reportedIds };
474
- } catch {
475
- return { reportedIds: [] };
476
- }
477
- }
478
- function selectReportable(candidates, reportedIds) {
479
- const reported = new Set(reportedIds);
480
- return candidates.filter((c) => c.size >= DIGEST_MIN_CLUSTER_SIZE && c.dismissedAt === void 0 && !reported.has(c.id)).sort((a, b) => b.size - a.size);
481
- }
482
- function formatDigestMessage(reportable) {
483
- const listed = reportable.slice(0, DIGEST_MAX_LISTED);
484
- const lines = listed.map((c) => `\xB7 ${c.taskSignature}\uFF08\u91CD\u590D ${c.size} \u6B21\uFF09`);
485
- const more = reportable.length - listed.length;
486
- const tail = more > 0 ? ` \u7B49 ${reportable.length} \u9879` : "";
487
- return `\u53D1\u73B0\u53EF\u6C89\u6DC0\u4E3A\u6280\u80FD\u7684\u91CD\u590D\u4EFB\u52A1${tail}\uFF1A
488
- ${lines.join("\n")}`;
489
- }
490
- async function runCandidateDigest(deps, tick) {
491
- const { storage, notify, logger } = deps;
492
- if (!notify) {
493
- return "missing-capability";
494
- }
495
- const state = await loadDigestState(storage);
496
- if (state.lastOccurrenceId === tick.occurrenceId) return "duplicate-occurrence";
497
- const { items } = await loadCandidates(storage);
498
- const reportable = selectReportable(items, state.reportedIds);
499
- if (reportable.length === 0) {
500
- await saveDigestState(storage, { ...state, lastOccurrenceId: tick.occurrenceId });
501
- return "nothing-to-report";
502
- }
503
- await notify({
504
- level: "info",
505
- code: "skill-inductor.candidates-ready",
506
- message: formatDigestMessage(reportable)
507
- });
508
- logger.info(`skill-inductor digest: notified ${reportable.length} candidate(s)`);
509
- await saveDigestState(storage, {
510
- // 只记录**已报告**的 id——被过滤掉的(规模不够/已拒绝)不入账,
511
- // 否则它们将来长大或被恢复时会因"报告过"而永不再提醒。
512
- reportedIds: [...state.reportedIds, ...reportable.map((c) => c.id)],
513
- lastOccurrenceId: tick.occurrenceId
514
- });
515
- return "notified";
516
- }
517
- async function saveDigestState(storage, state) {
518
- try {
519
- await storage.save(DIGEST_STATE_ID, { ...state });
520
- } catch {
521
- }
522
- }
523
-
524
- // ../../extensions/plugin-skill-inductor/plugin.ts
525
- var plugin_default = definePlugin(() => ({
526
- services: [
527
- {
528
- id: "inductor",
529
- /**
530
- * RFC-327 M4:候选摘要的定时报告。
531
- *
532
- * 这是 otto 第一条真实的"自迭代定时回路"——宿主 core schedule 到点 →
533
- * `schedule.tick` 投递到本隔离 worker → 只读自己的候选存储 → 有值得处理的事时
534
- * 经 `host.notify` 提示用户查看候选。常驻 service 不持有可验证的会话交互目标,故不得
535
- * 调用 `host.ask`(RFC-350 fail-closed);cron 解析/持久化/崩溃恢复全归 core(R4),
536
- * 本插件拿不到 timer handle。
537
- *
538
- * 每 4 小时一次:低频足以避免噪音,又能在用户结束一段工作后的合理窗口内提醒。
539
- * 没有新候选时**完全静默**(见 candidate-digest 的"静默是常态")。
540
- */
541
- schedules: [{ key: "candidate-digest", cron: "0 */4 * * *", name: "\u6280\u80FD\u5019\u9009\u6458\u8981" }],
542
- start(ctx) {
543
- const inductor = new SkillInductor({
544
- host: ctx.host,
545
- storage: ctx.storage,
546
- logger: ctx.logger
547
- });
548
- ctx.onNotify?.((n) => {
549
- if (n.type === "schedule.tick") {
550
- if (!ctx.storage) return;
551
- return runCandidateDigest(
552
- { storage: ctx.storage, notify: ctx.host?.notify, logger: ctx.logger },
553
- { scheduleId: n.scheduleId, occurrenceId: n.occurrenceId }
554
- ).then(() => void 0).catch(() => void 0);
555
- }
556
- if (n.type !== "session.idle") return;
557
- return inductor.onSessionIdle(n.sessionId, n.options).then(() => void 0).catch(() => void 0);
558
- });
559
- ctx.logger.info("skill-inductor service started");
560
- },
561
- stop() {
562
- }
563
- }
564
- ]
565
- }));