Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ A reworked chat composer, especially on mobile, agents that keep their memory an
- **Chat: attachments as preview tiles.** Images show a thumbnail and PDFs their first page; other files show a card. Click a tile to open the file, or use its corner × to remove it (#526).
- **Chat: Take photo on mobile.** The attach menu has a Take photo row that goes straight to the camera (#520).
- **Chat: one jump-to-bottom button** replaces the message-navigation arrows. It appears once you scroll away from the end, including while a reply streams below the fold (#522, #523, #527, #528).
- **Agents: where a skill came from and how it's used.** Skills the agent writes are marked as its own, and the Agent editor shows how often each custom skill has been used and revised (#516).
- **What's new, in the plugin.** After an update, a notice opens a What's new tab with the notes for every release since the one you last saw. The Show release notes command and a row in Settings → Troubleshooting open it any time (#533).
- **Update notices.** Once a day the plugin asks GitHub whether a newer version exists and says so, with a link to Community plugins. It sends nothing about you or your vault, stays quiet while Obsidian's own automatic update check is on, and can be turned off under Settings → General → Privacy (#534).

Expand Down
1 change: 0 additions & 1 deletion src/agent/AgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ export class AgentManager {
tools.push(
createLoadSkillTool(this.plugin.skillsService, {
skillNames: loadableSkills,
recordUsage: (name) => getData().recordSkillLoad(name),
isToolAvailable: (toolId) => {
if (BUILT_IN_TOOL_IDS.includes(toolId as BuiltInToolId)) {
return boundBuiltInTools.has(toolId);
Expand Down
14 changes: 1 addition & 13 deletions src/agent/tools/loadSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { tool } from "@langchain/core/tools";
import { z } from "zod";
import type { SkillsService } from "../../skills/SkillsService";
import type { CommunityPluginStatus } from "../integrations/pluginIntegrations";
import { Logger as Log } from "../../utils/logging";
import { recordSkillLoaded } from "./skillLoadRegistry";

/** Frontmatter key (under `metadata:`) listing community plugin ids a skill can make use of. */
Expand Down Expand Up @@ -47,12 +46,6 @@ export interface LoadSkillToolOptions {
* that path exists, instead of letting it find out from a failed result.
*/
pluginStatus?: (pluginId: string) => { status: CommunityPluginStatus; displayName: string };
/**
* Called after a skill's body was returned to the model. The plugin data store counts
* loads per skill (see `SkillUsageEntry`); injected rather than imported so the tool
* stays testable without a live store, and a missed record never fails a load.
*/
recordUsage?: (skillName: string) => void;
}

/**
Expand All @@ -64,7 +57,7 @@ export interface LoadSkillToolOptions {
* @returns A LangChain tool for loading skill content
*/
export function createLoadSkillTool(skillsService: SkillsService, options: LoadSkillToolOptions) {
const { skillNames, isToolAvailable, pluginStatus, recordUsage } = options;
const { skillNames, isToolAvailable, pluginStatus } = options;

// If no skills available, return a tool that explains this
if (skillNames.length === 0) {
Expand Down Expand Up @@ -95,11 +88,6 @@ export function createLoadSkillTool(skillsService: SkillsService, options: LoadS
// The model has now seen this skill's current text in this conversation, which is
// what `manage_skills` requires before it will patch or rewrite it.
recordSkillLoaded(config?.configurable?.thread_id, skillName, skill.content);
try {
recordUsage?.(skillName);
} catch (error) {
Log.warn(`load_skill: could not record usage of ${skillName}`, error);
}

// Return the skill content with metadata
const lines: string[] = [];
Expand Down
12 changes: 5 additions & 7 deletions src/agent/tools/manageSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ function splitSkillMd(raw: string): { head: string; body: string } | null {
}

/**
* `metadata.author` value stamped on every skill the agent creates. Bundled skills carry
* `S2B` in the same key and hand-written ones carry whatever the user put there (or nothing),
* so one existing key tells the three apart — which is what lets anything autonomous later
* (a curator) confine itself to the agent's own skills. A fixed word rather than the agent's
* name: consumers need no list of agent names, and a renamed or deleted agent changes nothing.
* `metadata.author` value stamped on every skill the agent creates: plain provenance in the
* same key the bundled skills use for `S2B`, written once and never read by the plugin. It is
* the one fact about a skill that cannot be reconstructed later, so it is recorded even though
* nothing acts on it today. A fixed word rather than the agent's name, so a renamed or deleted
* agent changes nothing.
*/
export const AGENT_SKILL_AUTHOR = "agent";

Expand Down Expand Up @@ -331,7 +331,6 @@ export function createManageSkillsTool(skillsService: SkillsService | undefined,
// counters so a later skill of the same name starts from zero.
const agent = getData().getAgent(agentId) ?? getData().getSelectedAgent();
if (agent?.skills[input.name]) delete agent.skills[input.name];
getData().forgetSkillUsage(input.name);

return `Deleted the "${input.name}" skill.`;
}
Expand Down Expand Up @@ -408,7 +407,6 @@ export function createManageSkillsTool(skillsService: SkillsService | undefined,
// history, so a revised core skill is flagged as customized rather than overwritten
// by the next upgrade.
await skillsService.writeSkillFile(skillName, newContent);
getData().recordSkillRevision(skillName);
// The model wrote this text too, so a follow-up revision in the same turn needs no reload.
recordSkillLoaded(threadId, skillName, parseFrontmatter(newContent).body);

Expand Down
4 changes: 0 additions & 4 deletions src/components/modal/AgentEditorModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { agentDefinitionPath, agentDir } from "../../utils/agentPaths";
import { Logger } from "../../utils/logging";
import { extractErrorMessage } from "../../utils/errorMessage";
import { humanizeSkillName } from "../../skills";
import { skillUsageSummary } from "../../skills/usageSummary";
import {
DEFAULT_AGENT_ICON,
type AgentConfig,
Expand Down Expand Up @@ -299,8 +298,6 @@ const skills = $derived.by(() => {
category: metadata.category ?? "custom",
corePluginId: metadata.corePluginId,
linkedPluginId: metadata.linkedPluginId,
author: metadata.frontmatter.metadata?.author,
usage: pluginData.getSkillUsage(skillName),
});
}
return result;
Expand Down Expand Up @@ -1052,7 +1049,6 @@ function getServerToolsState(serverId: string): MCPServerToolsState | undefined
class="skill-entity"
name={ext.displayName}
desc={ext.description}
meta={skillUsageSummary(ext.author, ext.usage)}
>
{#snippet actions()}
<Button
Expand Down
10 changes: 0 additions & 10 deletions src/skills/SkillsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,16 +1018,6 @@ export class SkillsService {
}

this.skillsCache.delete(skillName);
// Drop the usage counters with the skill, whichever surface deleted it (the Agent
// editor's trash button lands here; manage_skills removes the folder itself and
// forgets on its own). A later skill of the same name must start from zero.
// Advisory data: a store that is not up yet must not turn a completed delete
// into a reported failure.
try {
getData().forgetSkillUsage(skillName);
} catch {
Log.debug(`Could not clear usage counters for ${skillName}`);
}
Log.info(`Deleted skill: ${skillName}`);
return true;
} catch (error) {
Expand Down
55 changes: 0 additions & 55 deletions src/skills/usageSummary.ts

This file was deleted.

58 changes: 0 additions & 58 deletions src/stores/dataStore.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import type {
PrivacyMode,
PromptFileReader,
RecentNoteEntry,
SkillUsageEntry,
StaleGuidance,
ToolConfig,
} from "../types/plugin";
Expand Down Expand Up @@ -259,7 +258,6 @@ export const DEFAULT_SETTINGS: PluginData = {
searchShowMatchContext: true,
searchShowKeyboardHints: true,
recentNotes: [],
skillUsage: [],
embeddingIndexes: [],
searchEmbedIndex: null,
graphEmbedIndex: null,
Expand Down Expand Up @@ -1352,62 +1350,6 @@ export class PluginDataStore {
void this.saveSettings();
}

// --- Skill usage ---

/**
* The usage list, healed to an empty list when the field is absent or not a list (a data
* file from before the field existed, or one written while the shape was still changing).
*/
private usageList(): SkillUsageEntry[] {
if (!Array.isArray(this.#data.skillUsage)) this.#data.skillUsage = [];
return this.#data.skillUsage;
}

/** Usage counters for a skill, or undefined when it has never been loaded or revised. */
getSkillUsage(skillName: string): SkillUsageEntry | undefined {
return this.usageList().find((entry) => entry.name === skillName);
}

/** All usage counters. */
get skillUsage(): readonly SkillUsageEntry[] {
return this.usageList();
}

private bumpSkillUsage(skillName: string, mutate: (entry: SkillUsageEntry) => void): void {
const usage = this.usageList();
let entry = usage.find((candidate) => candidate.name === skillName);
if (!entry) {
entry = { name: skillName, loadCount: 0, lastLoadedAt: null, revisionCount: 0, lastRevisedAt: null };
usage.push(entry);
}
mutate(entry);
void this.saveSettings();
}

/** `load_skill` returned this skill's body. */
recordSkillLoad(skillName: string): void {
this.bumpSkillUsage(skillName, (entry) => {
entry.loadCount += 1;
entry.lastLoadedAt = Date.now();
});
}

/** `manage_skills` patched or rewrote this skill. */
recordSkillRevision(skillName: string): void {
this.bumpSkillUsage(skillName, (entry) => {
entry.revisionCount += 1;
entry.lastRevisedAt = Date.now();
});
}

/** The skill is gone; a later skill of the same name starts from zero. */
forgetSkillUsage(skillName: string): void {
const usage = this.usageList();
if (!usage.some((entry) => entry.name === skillName)) return;
this.#data.skillUsage = usage.filter((entry) => entry.name !== skillName);
void this.saveSettings();
}

// --- Embedding Indexes (Multi-Index) ---

get embeddingIndexes(): EmbeddingIndexConfig[] {
Expand Down
29 changes: 0 additions & 29 deletions src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,6 @@ export interface RecentNoteEntry {
lastOpenedAt: number;
}

/**
* How a skill has been used, stored in plugin data (never in the SKILL.md: a counter in
* frontmatter would turn every load into a vault write, break the bundled skills'
* shipped-history fingerprint, and diverge across synced devices). Advisory only — a missed
* bump costs nothing — and per device, since plugin data is.
*
* A list rather than a record keyed by name, like `recentNotes`: skill names are free-form
* ("constructor" is a valid one), and Svelte's `$state` proxy silently drops a write to a key
* that exists on the prototype chain, so a name-keyed object could never store such a skill.
*/
export interface SkillUsageEntry {
/** The skill's frontmatter name. */
name: string;
/** Times `load_skill` returned this skill's body. */
loadCount: number;
/** Epoch ms of the most recent load, or null when never loaded. */
lastLoadedAt: number | null;
/** Times `manage_skills` patched or rewrote this skill. */
revisionCount: number;
/** Epoch ms of the most recent revision, or null when never revised. */
lastRevisedAt: number | null;
}

/**
* How long after being opened a note still counts as "recent".
*
Expand Down Expand Up @@ -323,10 +300,6 @@ export interface SkillDisplayInfo {
linkedPluginId?: string;
/** Linked core plugin ID (if any) */
corePluginId?: string;
/** `metadata.author` from the frontmatter: `S2B` for bundled skills, `agent` for agent-created ones. */
author?: string;
/** Usage counters from plugin data, when the skill has ever been loaded or revised. */
usage?: SkillUsageEntry;
}

// ============================================================================
Expand Down Expand Up @@ -646,8 +619,6 @@ export interface PluginData {
searchShowMatchContext: boolean;
searchShowKeyboardHints: boolean;
recentNotes: RecentNoteEntry[];
/** Per-skill load and revision counters (see {@link SkillUsageEntry}); one entry per skill name. */
skillUsage: SkillUsageEntry[];

/**
* Registry of all known embedding indexes.
Expand Down
24 changes: 0 additions & 24 deletions test/agent/loadSkill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,4 @@ describe("load_skill tool", () => {
expect(skillLoadState("t-load", "views", "views instructions")).toBe("not-loaded");
expect(skillLoadState("t-other", "web", "web instructions")).toBe("not-loaded");
});

it("reports a successful load to the usage recorder, and only then", async () => {
const recordUsage = vi.fn();
const service = mockSkillsService({ web: {} });
const tool = createLoadSkillTool(service, { skillNames: ["web", "gone"], recordUsage });

await tool.invoke({ skillName: "web" });
await tool.invoke({ skillName: "gone" }); // known to the tool, missing from the service

expect(recordUsage).toHaveBeenCalledTimes(1);
expect(recordUsage).toHaveBeenCalledWith("web");
});

// A recorder that throws must never cost the model the skill it asked for.
it("still returns the skill when the usage recorder throws", async () => {
const service = mockSkillsService({ web: { body: "Use fetch_url." } });
const tool = createLoadSkillTool(service, {
skillNames: ["web"],
recordUsage: () => {
throw new Error("store gone");
},
});
expect(String(await tool.invoke({ skillName: "web" }))).toContain("Use fetch_url.");
});
});
Loading
Loading