@rubytech/taskmaster 1.5.1 → 1.5.3
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/dist/agents/skills/workspace.js +23 -2
- package/dist/agents/taskmaster-tools.js +8 -0
- package/dist/agents/tool-policy.js +4 -0
- package/dist/agents/tools/skill-draft-save-tool.js +110 -0
- package/dist/auto-reply/tokens.js +23 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +1 -0
- package/dist/config/defaults.js +32 -0
- package/dist/config/io.js +4 -4
- package/dist/config/legacy.migrations.part-3.js +51 -0
- package/dist/config/validation.js +2 -2
- package/dist/control-ui/assets/{index-D-PHW4mO.js → index-C12OTik-.js} +734 -614
- package/dist/control-ui/assets/index-C12OTik-.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/isolated-agent/delivery-record.js +54 -0
- package/dist/cron/isolated-agent/history.js +221 -0
- package/dist/cron/isolated-agent/run.js +56 -2
- package/dist/gateway/protocol/index.js +2 -1
- package/dist/gateway/protocol/schema/agents-models-skills.js +9 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -1
- package/dist/gateway/server-methods/skills.js +29 -5
- package/dist/gateway/server.impl.js +17 -0
- package/dist/memory/audit.js +19 -2
- package/dist/memory/manager.js +1 -0
- package/package.json +1 -1
- package/skills/skill-builder/SKILL.md +13 -8
- package/taskmaster-docs/USER-GUIDE.md +67 -8
- package/templates/customer/agents/admin/BOOTSTRAP.md +1 -1
- package/dist/control-ui/assets/index-D-PHW4mO.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-C12OTik-.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-D6WTmXJM.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Writes a delivery record to the agent's workspace memory so the admin agent
|
|
5
|
+
* can discover what was sent by cron jobs when recipients reply later.
|
|
6
|
+
*
|
|
7
|
+
* Files are stored in `memory/shared/cron-activity/` as dated markdown,
|
|
8
|
+
* discoverable via the agent's normal memory search.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeCronDeliveryRecord(params) {
|
|
11
|
+
const { workspaceDir, jobName, jobId, agentOutput, deliveredTo, timestamp } = params;
|
|
12
|
+
if (!agentOutput.trim() || deliveredTo.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const dir = path.join(workspaceDir, "memory", "shared", "cron-activity");
|
|
15
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
16
|
+
const dateSlug = formatDateSlug(timestamp);
|
|
17
|
+
const nameSlug = slugify(jobName);
|
|
18
|
+
const idSuffix = jobId.slice(0, 8);
|
|
19
|
+
const fileName = `${dateSlug}-${nameSlug}-${idSuffix}.md`;
|
|
20
|
+
const filePath = path.join(dir, fileName);
|
|
21
|
+
const recipientLines = deliveredTo.map((d) => `- ${d.to} (${d.channel})`).join("\n");
|
|
22
|
+
const content = [
|
|
23
|
+
`# Cron delivery: ${jobName}`,
|
|
24
|
+
"",
|
|
25
|
+
`**Time:** ${timestamp.toISOString()}`,
|
|
26
|
+
`**Job ID:** ${jobId}`,
|
|
27
|
+
"",
|
|
28
|
+
"**Delivered to:**",
|
|
29
|
+
recipientLines,
|
|
30
|
+
"",
|
|
31
|
+
"## Message sent",
|
|
32
|
+
"",
|
|
33
|
+
agentOutput.trim(),
|
|
34
|
+
"",
|
|
35
|
+
].join("\n");
|
|
36
|
+
await fs.promises.writeFile(filePath, content, "utf-8");
|
|
37
|
+
return filePath;
|
|
38
|
+
}
|
|
39
|
+
function formatDateSlug(date) {
|
|
40
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
41
|
+
return [
|
|
42
|
+
String(date.getFullYear()),
|
|
43
|
+
pad(date.getMonth() + 1),
|
|
44
|
+
pad(date.getDate()),
|
|
45
|
+
pad(date.getHours()) + pad(date.getMinutes()) + pad(date.getSeconds()),
|
|
46
|
+
].join("-");
|
|
47
|
+
}
|
|
48
|
+
function slugify(text) {
|
|
49
|
+
return text
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
52
|
+
.replace(/^-+|-+$/g, "")
|
|
53
|
+
.slice(0, 40);
|
|
54
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
|
|
4
|
+
/** Max characters per individual message in the context block. */
|
|
5
|
+
const MAX_MESSAGE_CHARS = 500;
|
|
6
|
+
/** Default number of recent user turns to include per session. */
|
|
7
|
+
const DEFAULT_TURNS_PER_SESSION = 5;
|
|
8
|
+
/** Default number of most-recently-active sessions to include. */
|
|
9
|
+
const DEFAULT_MAX_SESSIONS = 15;
|
|
10
|
+
/** Default cap on total user turns across all sessions combined. */
|
|
11
|
+
const DEFAULT_MAX_TOTAL_TURNS = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Loads recent conversation messages from all non-cron sessions for a given agent
|
|
14
|
+
* and formats them as a context block for prepending to a cron job prompt.
|
|
15
|
+
*
|
|
16
|
+
* This enables isolated cron jobs (briefings, periodic checks) to be aware of
|
|
17
|
+
* recent agent interactions across all channels — WhatsApp, iMessage, web chat, etc.
|
|
18
|
+
*/
|
|
19
|
+
export function loadCrossChannelHistory(params) {
|
|
20
|
+
const turnsPerSession = params.turnsPerSession ?? DEFAULT_TURNS_PER_SESSION;
|
|
21
|
+
const maxSessions = params.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
22
|
+
const maxTotalTurns = params.maxTotalTurns ?? DEFAULT_MAX_TOTAL_TURNS;
|
|
23
|
+
const storePath = resolveStorePath(params.cfg.session?.store, {
|
|
24
|
+
agentId: params.agentId,
|
|
25
|
+
});
|
|
26
|
+
let store;
|
|
27
|
+
try {
|
|
28
|
+
store = loadSessionStore(storePath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
const sessionsDir = path.dirname(storePath);
|
|
34
|
+
// Include only conversational sessions — exclude cron, hook, node, and subagent sessions.
|
|
35
|
+
const sessions = Object.entries(store)
|
|
36
|
+
.filter(([key]) => shouldIncludeSession(key))
|
|
37
|
+
.sort(([, a], [, b]) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
|
|
38
|
+
.slice(0, maxSessions);
|
|
39
|
+
if (sessions.length === 0)
|
|
40
|
+
return "";
|
|
41
|
+
const blocks = [];
|
|
42
|
+
let totalTurns = 0;
|
|
43
|
+
for (const [sessionKey, entry] of sessions) {
|
|
44
|
+
const remaining = maxTotalTurns - totalTurns;
|
|
45
|
+
if (remaining <= 0)
|
|
46
|
+
break;
|
|
47
|
+
if (!entry.sessionId)
|
|
48
|
+
continue;
|
|
49
|
+
const transcriptPath = entry.sessionFile?.trim() || path.join(sessionsDir, `${entry.sessionId}.jsonl`);
|
|
50
|
+
const effectiveLimit = Math.min(turnsPerSession, remaining);
|
|
51
|
+
const turns = readRecentConversationTurns(transcriptPath, effectiveLimit);
|
|
52
|
+
if (turns.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
totalTurns += turns.filter((t) => t.role === "user").length;
|
|
55
|
+
const label = formatSessionLabel(sessionKey, entry);
|
|
56
|
+
const formatted = turns
|
|
57
|
+
.map((t) => {
|
|
58
|
+
const role = t.role === "user" ? "User" : "Assistant";
|
|
59
|
+
return ` ${role}: ${truncate(t.text, MAX_MESSAGE_CHARS)}`;
|
|
60
|
+
})
|
|
61
|
+
.join("\n");
|
|
62
|
+
blocks.push(`[${label}]\n${formatted}`);
|
|
63
|
+
}
|
|
64
|
+
if (blocks.length === 0)
|
|
65
|
+
return "";
|
|
66
|
+
return [
|
|
67
|
+
"--- Recent conversations across all channels (for context) ---",
|
|
68
|
+
"",
|
|
69
|
+
blocks.join("\n\n"),
|
|
70
|
+
"",
|
|
71
|
+
"--- End of recent conversations ---",
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/** Extract the session-specific portion after "agent:{agentId}:". */
|
|
78
|
+
function extractSessionRest(key) {
|
|
79
|
+
const match = key.match(/^agent:[^:]+:(.+)$/);
|
|
80
|
+
return match?.[1] ?? key;
|
|
81
|
+
}
|
|
82
|
+
/** Only include conversational sessions — not system/background sessions. */
|
|
83
|
+
function shouldIncludeSession(key) {
|
|
84
|
+
const rest = extractSessionRest(key);
|
|
85
|
+
if (rest.startsWith("cron:"))
|
|
86
|
+
return false;
|
|
87
|
+
if (rest.startsWith("hook:"))
|
|
88
|
+
return false;
|
|
89
|
+
if (rest.startsWith("node:") || rest.startsWith("node-"))
|
|
90
|
+
return false;
|
|
91
|
+
if (rest.startsWith("subagent:"))
|
|
92
|
+
return false;
|
|
93
|
+
if (rest.startsWith("acp:"))
|
|
94
|
+
return false;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads the last N conversation turns (user + assistant text) from a JSONL
|
|
99
|
+
* transcript file. Tool calls and tool results are stripped.
|
|
100
|
+
*/
|
|
101
|
+
function readRecentConversationTurns(transcriptPath, maxUserTurns) {
|
|
102
|
+
let content;
|
|
103
|
+
try {
|
|
104
|
+
content = fs.readFileSync(transcriptPath, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
const allTurns = [];
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (!line.trim())
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(line);
|
|
116
|
+
const msg = parsed?.message;
|
|
117
|
+
if (!msg?.role)
|
|
118
|
+
continue;
|
|
119
|
+
if (msg.role !== "user" && msg.role !== "assistant")
|
|
120
|
+
continue;
|
|
121
|
+
const text = extractMessageText(msg);
|
|
122
|
+
if (!text)
|
|
123
|
+
continue;
|
|
124
|
+
allTurns.push({ role: msg.role, text });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// skip malformed lines
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return limitToLastUserTurns(allTurns, maxUserTurns);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Keeps the last N user turns plus their associated assistant responses.
|
|
134
|
+
* Same logic as the main session history's `limitHistoryTurns`.
|
|
135
|
+
*/
|
|
136
|
+
function limitToLastUserTurns(turns, limit) {
|
|
137
|
+
if (turns.length === 0 || limit <= 0)
|
|
138
|
+
return [];
|
|
139
|
+
let userCount = 0;
|
|
140
|
+
let lastUserIndex = turns.length;
|
|
141
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
142
|
+
if (turns[i].role === "user") {
|
|
143
|
+
userCount++;
|
|
144
|
+
if (userCount > limit) {
|
|
145
|
+
return turns.slice(lastUserIndex);
|
|
146
|
+
}
|
|
147
|
+
lastUserIndex = i;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return turns;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Extracts text content from an agent message, handling both string content
|
|
154
|
+
* and content-block arrays. Tool-use blocks are excluded.
|
|
155
|
+
*/
|
|
156
|
+
function extractMessageText(msg) {
|
|
157
|
+
const content = msg.content;
|
|
158
|
+
if (typeof content === "string")
|
|
159
|
+
return content.trim() || null;
|
|
160
|
+
if (!Array.isArray(content))
|
|
161
|
+
return null;
|
|
162
|
+
const parts = [];
|
|
163
|
+
for (const block of content) {
|
|
164
|
+
if (!block || typeof block !== "object")
|
|
165
|
+
continue;
|
|
166
|
+
const b = block;
|
|
167
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
168
|
+
const trimmed = b.text.trim();
|
|
169
|
+
if (trimmed)
|
|
170
|
+
parts.push(trimmed);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return parts.join("\n") || null;
|
|
174
|
+
}
|
|
175
|
+
/** Human-readable label for a session key, e.g. "WhatsApp DM Joel (+447504472444)". */
|
|
176
|
+
function formatSessionLabel(sessionKey, entry) {
|
|
177
|
+
const rest = extractSessionRest(sessionKey);
|
|
178
|
+
const name = entry.displayName?.trim();
|
|
179
|
+
if (rest === "main")
|
|
180
|
+
return "Main session";
|
|
181
|
+
// per-peer DM: dm:{peerId}
|
|
182
|
+
const dmMatch = rest.match(/^dm:(.+)$/);
|
|
183
|
+
if (dmMatch) {
|
|
184
|
+
const peerId = dmMatch[1];
|
|
185
|
+
const channel = entry.lastChannel ?? entry.channel;
|
|
186
|
+
const label = name ? `${name} (${peerId})` : peerId;
|
|
187
|
+
if (channel)
|
|
188
|
+
return `${capitalizeChannel(channel)} DM ${label}`;
|
|
189
|
+
return `DM ${label}`;
|
|
190
|
+
}
|
|
191
|
+
// per-channel-peer DM: {channel}:dm:{peerId}
|
|
192
|
+
const channelDmMatch = rest.match(/^([^:]+):dm:(.+)$/);
|
|
193
|
+
if (channelDmMatch) {
|
|
194
|
+
const peerId = channelDmMatch[2];
|
|
195
|
+
const label = name ? `${name} (${peerId})` : peerId;
|
|
196
|
+
return `${capitalizeChannel(channelDmMatch[1])} DM ${label}`;
|
|
197
|
+
}
|
|
198
|
+
// group/channel: {channel}:{group|channel}:{peerId}
|
|
199
|
+
const groupMatch = rest.match(/^([^:]+):(group|channel):(.+)$/);
|
|
200
|
+
if (groupMatch) {
|
|
201
|
+
const subject = entry.subject?.trim();
|
|
202
|
+
const peerId = groupMatch[3];
|
|
203
|
+
const label = subject ?? peerId;
|
|
204
|
+
return `${capitalizeChannel(groupMatch[1])} ${groupMatch[2]} ${label}`;
|
|
205
|
+
}
|
|
206
|
+
return rest;
|
|
207
|
+
}
|
|
208
|
+
function capitalizeChannel(channel) {
|
|
209
|
+
if (channel === "whatsapp")
|
|
210
|
+
return "WhatsApp";
|
|
211
|
+
if (channel === "imessage")
|
|
212
|
+
return "iMessage";
|
|
213
|
+
if (channel === "webchat")
|
|
214
|
+
return "Web chat";
|
|
215
|
+
return channel.charAt(0).toUpperCase() + channel.slice(1);
|
|
216
|
+
}
|
|
217
|
+
function truncate(text, maxChars) {
|
|
218
|
+
if (text.length <= maxChars)
|
|
219
|
+
return text;
|
|
220
|
+
return `${text.slice(0, maxChars)}…`;
|
|
221
|
+
}
|
|
@@ -25,6 +25,10 @@ import { resolveAgentBoundAccountId } from "../../routing/bindings.js";
|
|
|
25
25
|
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
26
26
|
import { expandCronRecipients } from "./recipients.js";
|
|
27
27
|
import { isHeartbeatOnlyResponse, pickLastNonEmptyTextFromPayloads, pickSummaryFromOutput, pickSummaryFromPayloads, resolveHeartbeatAckMaxChars, } from "./helpers.js";
|
|
28
|
+
import { appendAssistantMessageToSessionTranscript } from "../../config/sessions/transcript.js";
|
|
29
|
+
import { resolveOutboundSessionRoute } from "../../infra/outbound/outbound-session.js";
|
|
30
|
+
import { writeCronDeliveryRecord } from "./delivery-record.js";
|
|
31
|
+
import { loadCrossChannelHistory } from "./history.js";
|
|
28
32
|
import { resolveCronSession } from "./session.js";
|
|
29
33
|
function matchesMessagingToolDeliveryTarget(target, delivery) {
|
|
30
34
|
if (!delivery.to || !target.to)
|
|
@@ -174,6 +178,19 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
174
178
|
const formattedTime = formatUserTime(new Date(now), userTimezone, userTimeFormat) ?? new Date(now).toISOString();
|
|
175
179
|
const timeLine = `Current time: ${formattedTime} (${userTimezone})`;
|
|
176
180
|
const commandBody = `${base}\n${timeLine}`.trim();
|
|
181
|
+
// Inject recent conversation history from all channels so the cron job
|
|
182
|
+
// has awareness of what has been discussed across WhatsApp, iMessage, web chat, etc.
|
|
183
|
+
const historyBlock = loadCrossChannelHistory({ cfg: params.cfg, agentId });
|
|
184
|
+
// When delivery is configured, the agent's response IS the message that gets sent.
|
|
185
|
+
// Instruct the agent to always produce a deliverable message — NO_REPLY would
|
|
186
|
+
// suppress delivery and defeat the purpose of the cron job.
|
|
187
|
+
const deliveryInstruction = deliveryRequested && hasExplicitTarget
|
|
188
|
+
? "\n\nDELIVERY MODE: Your text response IS the message that will be sent to the recipient automatically. Write it as the final deliverable. The silent-reply rules (NO_REPLY) do not apply here — this is a scheduled job that always needs output. Do not use the message tool; delivery is handled for you."
|
|
189
|
+
: "";
|
|
190
|
+
const fullPrompt = [commandBody, historyBlock, deliveryInstruction]
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.join("\n\n")
|
|
193
|
+
.trim();
|
|
177
194
|
const existingSnapshot = cronSession.sessionEntry.skillsSnapshot;
|
|
178
195
|
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
|
179
196
|
const needsSkillsSnapshot = !existingSnapshot || existingSnapshot.version !== skillsSnapshotVersion;
|
|
@@ -228,7 +245,7 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
228
245
|
sessionFile,
|
|
229
246
|
workspaceDir,
|
|
230
247
|
config: cfgWithAgentDefaults,
|
|
231
|
-
prompt:
|
|
248
|
+
prompt: fullPrompt,
|
|
232
249
|
provider: providerOverride,
|
|
233
250
|
model: modelOverride,
|
|
234
251
|
thinkLevel,
|
|
@@ -247,7 +264,7 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
247
264
|
workspaceDir,
|
|
248
265
|
config: cfgWithAgentDefaults,
|
|
249
266
|
skillsSnapshot,
|
|
250
|
-
prompt:
|
|
267
|
+
prompt: fullPrompt,
|
|
251
268
|
lane: params.lane ?? "cron",
|
|
252
269
|
provider: providerOverride,
|
|
253
270
|
model: modelOverride,
|
|
@@ -363,6 +380,43 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
363
380
|
errors.push(`${targetTo}: ${String(err)}`);
|
|
364
381
|
}
|
|
365
382
|
}
|
|
383
|
+
// Mirror delivered message into each recipient's DM session transcript
|
|
384
|
+
// so the agent sees it in conversation history when the recipient replies.
|
|
385
|
+
// This is the same mechanism the message tool uses (delivery-mirror).
|
|
386
|
+
if (outputText) {
|
|
387
|
+
for (const targetTo of deliveryTargets) {
|
|
388
|
+
const route = await resolveOutboundSessionRoute({
|
|
389
|
+
cfg: cfgWithAgentDefaults,
|
|
390
|
+
channel: resolvedDelivery.channel,
|
|
391
|
+
agentId,
|
|
392
|
+
accountId: resolvedDelivery.accountId,
|
|
393
|
+
target: targetTo,
|
|
394
|
+
});
|
|
395
|
+
if (route) {
|
|
396
|
+
await appendAssistantMessageToSessionTranscript({
|
|
397
|
+
agentId,
|
|
398
|
+
sessionKey: route.sessionKey,
|
|
399
|
+
text: outputText,
|
|
400
|
+
}).catch(() => {
|
|
401
|
+
/* best-effort — mirror is helpful but must not block delivery */
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Also persist delivery record to memory for searchability.
|
|
406
|
+
await writeCronDeliveryRecord({
|
|
407
|
+
workspaceDir,
|
|
408
|
+
jobName: params.job.name,
|
|
409
|
+
jobId: params.job.id,
|
|
410
|
+
agentOutput: outputText,
|
|
411
|
+
deliveredTo: deliveryTargets.map((to) => ({
|
|
412
|
+
to,
|
|
413
|
+
channel: resolvedDelivery.channel,
|
|
414
|
+
})),
|
|
415
|
+
timestamp: new Date(),
|
|
416
|
+
}).catch(() => {
|
|
417
|
+
/* best-effort — delivery record is helpful but not required */
|
|
418
|
+
});
|
|
419
|
+
}
|
|
366
420
|
if (errors.length > 0 && !bestEffortDeliver) {
|
|
367
421
|
return {
|
|
368
422
|
status: "error",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AjvPkg from "ajv";
|
|
2
|
-
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, AgentWaitParamsSchema, ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, ChatAbortParamsSchema, ChatEventSchema, ChatHistoryParamsSchema, ChatInjectParamsSchema, ChatSendParamsSchema, ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, ConnectParamsSchema, CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, DevicePairApproveParamsSchema, DevicePairListParamsSchema, DevicePairRejectParamsSchema, DeviceTokenRevokeParamsSchema, DeviceTokenRotateParamsSchema, ExecApprovalsGetParamsSchema, ExecApprovalsNodeGetParamsSchema, ExecApprovalsNodeSetParamsSchema, ExecApprovalsSetParamsSchema, ExecApprovalRequestParamsSchema, ExecApprovalResolveParamsSchema, ErrorCodes, ErrorShapeSchema, EventFrameSchema, errorShape, GatewayFrameSchema, HelloOkSchema, LogsTailParamsSchema, LogsTailResultSchema, SessionsTranscriptParamsSchema, SessionsTranscriptResultSchema, ModelsListParamsSchema, NodeDescribeParamsSchema, NodeEventParamsSchema, NodeInvokeParamsSchema, NodeInvokeResultParamsSchema, NodeListParamsSchema, NodePairApproveParamsSchema, NodePairListParamsSchema, NodePairRejectParamsSchema, NodePairRequestParamsSchema, NodePairVerifyParamsSchema, NodeRenameParamsSchema, PollParamsSchema, PROTOCOL_VERSION, PresenceEntrySchema, ProtocolSchemas, RequestFrameSchema, ResponseFrameSchema, SendParamsSchema, SessionsCompactParamsSchema, SessionsDeleteParamsSchema, SessionsListParamsSchema, SessionsPatchParamsSchema, SessionsPreviewParamsSchema, SessionsResetParamsSchema, SessionsResolveParamsSchema, ShutdownEventSchema, SkillsBinsParamsSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, SnapshotSchema, StateVersionSchema, TalkModeParamsSchema, TickEventSchema, UpdateRunParamsSchema, WakeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, WizardCancelParamsSchema, WizardNextParamsSchema, WizardNextResultSchema, WizardStartParamsSchema, WizardStartResultSchema, WizardStatusParamsSchema, WizardStatusResultSchema, WizardStepSchema, } from "./schema.js";
|
|
2
|
+
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, AgentWaitParamsSchema, ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, ChatAbortParamsSchema, ChatEventSchema, ChatHistoryParamsSchema, ChatInjectParamsSchema, ChatSendParamsSchema, ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, ConnectParamsSchema, CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, DevicePairApproveParamsSchema, DevicePairListParamsSchema, DevicePairRejectParamsSchema, DeviceTokenRevokeParamsSchema, DeviceTokenRotateParamsSchema, ExecApprovalsGetParamsSchema, ExecApprovalsNodeGetParamsSchema, ExecApprovalsNodeSetParamsSchema, ExecApprovalsSetParamsSchema, ExecApprovalRequestParamsSchema, ExecApprovalResolveParamsSchema, ErrorCodes, ErrorShapeSchema, EventFrameSchema, errorShape, GatewayFrameSchema, HelloOkSchema, LogsTailParamsSchema, LogsTailResultSchema, SessionsTranscriptParamsSchema, SessionsTranscriptResultSchema, ModelsListParamsSchema, NodeDescribeParamsSchema, NodeEventParamsSchema, NodeInvokeParamsSchema, NodeInvokeResultParamsSchema, NodeListParamsSchema, NodePairApproveParamsSchema, NodePairListParamsSchema, NodePairRejectParamsSchema, NodePairRequestParamsSchema, NodePairVerifyParamsSchema, NodeRenameParamsSchema, PollParamsSchema, PROTOCOL_VERSION, PresenceEntrySchema, ProtocolSchemas, RequestFrameSchema, ResponseFrameSchema, SendParamsSchema, SessionsCompactParamsSchema, SessionsDeleteParamsSchema, SessionsListParamsSchema, SessionsPatchParamsSchema, SessionsPreviewParamsSchema, SessionsResetParamsSchema, SessionsResolveParamsSchema, ShutdownEventSchema, SkillsBinsParamsSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsSaveDraftParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, SnapshotSchema, StateVersionSchema, TalkModeParamsSchema, TickEventSchema, UpdateRunParamsSchema, WakeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, WizardCancelParamsSchema, WizardNextParamsSchema, WizardNextResultSchema, WizardStartParamsSchema, WizardStartResultSchema, WizardStatusParamsSchema, WizardStatusResultSchema, WizardStepSchema, } from "./schema.js";
|
|
3
3
|
const ajv = new AjvPkg({
|
|
4
4
|
allErrors: true,
|
|
5
5
|
strict: false,
|
|
@@ -55,6 +55,7 @@ export const validateSkillsReadParams = ajv.compile(SkillsReadParamsSchema);
|
|
|
55
55
|
export const validateSkillsCreateParams = ajv.compile(SkillsCreateParamsSchema);
|
|
56
56
|
export const validateSkillsDeleteParams = ajv.compile(SkillsDeleteParamsSchema);
|
|
57
57
|
export const validateSkillsDraftsParams = ajv.compile(SkillsDraftsParamsSchema);
|
|
58
|
+
export const validateSkillsSaveDraftParams = ajv.compile(SkillsSaveDraftParamsSchema);
|
|
58
59
|
export const validateSkillsDeleteDraftParams = ajv.compile(SkillsDeleteDraftParamsSchema);
|
|
59
60
|
export const validateCronListParams = ajv.compile(CronListParamsSchema);
|
|
60
61
|
export const validateCronStatusParams = ajv.compile(CronStatusParamsSchema);
|
|
@@ -65,6 +65,15 @@ export const SkillsDeleteParamsSchema = Type.Object({
|
|
|
65
65
|
name: NonEmptyString,
|
|
66
66
|
}, { additionalProperties: false });
|
|
67
67
|
export const SkillsDraftsParamsSchema = Type.Object({}, { additionalProperties: false });
|
|
68
|
+
export const SkillsSaveDraftParamsSchema = Type.Object({
|
|
69
|
+
name: Type.String({
|
|
70
|
+
pattern: "^[a-z0-9][a-z0-9_-]*$",
|
|
71
|
+
minLength: 1,
|
|
72
|
+
description: "Skill directory name (lowercase, hyphens, underscores)",
|
|
73
|
+
}),
|
|
74
|
+
skillContent: NonEmptyString,
|
|
75
|
+
references: Type.Optional(Type.Array(SkillReferenceFileSchema)),
|
|
76
|
+
}, { additionalProperties: false });
|
|
68
77
|
export const SkillsDeleteDraftParamsSchema = Type.Object({
|
|
69
78
|
name: NonEmptyString,
|
|
70
79
|
}, { additionalProperties: false });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentWaitParamsSchema, PollParamsSchema, SendParamsSchema, WakeParamsSchema, } from "./agent.js";
|
|
2
|
-
import { AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, ModelChoiceSchema, ModelsListParamsSchema, ModelsListResultSchema, SkillsBinsParamsSchema, SkillsBinsResultSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, } from "./agents-models-skills.js";
|
|
2
|
+
import { AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, ModelChoiceSchema, ModelsListParamsSchema, ModelsListResultSchema, SkillsBinsParamsSchema, SkillsBinsResultSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsSaveDraftParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, } from "./agents-models-skills.js";
|
|
3
3
|
import { ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, TalkModeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, } from "./channels.js";
|
|
4
4
|
import { ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, UpdateRunParamsSchema, } from "./config.js";
|
|
5
5
|
import { CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunLogEntrySchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, } from "./cron.js";
|
|
@@ -84,6 +84,7 @@ export const ProtocolSchemas = {
|
|
|
84
84
|
SkillsCreateParams: SkillsCreateParamsSchema,
|
|
85
85
|
SkillsDeleteParams: SkillsDeleteParamsSchema,
|
|
86
86
|
SkillsDraftsParams: SkillsDraftsParamsSchema,
|
|
87
|
+
SkillsSaveDraftParams: SkillsSaveDraftParamsSchema,
|
|
87
88
|
SkillsDeleteDraftParams: SkillsDeleteDraftParamsSchema,
|
|
88
89
|
CronJob: CronJobSchema,
|
|
89
90
|
CronListParams: CronListParamsSchema,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
|
3
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgentId, } from "../../agents/agent-scope.js";
|
|
4
4
|
import { installSkill } from "../../agents/skills-install.js";
|
|
5
5
|
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
|
6
|
-
import { loadWorkspaceSkillEntries, resolveBundledSkillsDir } from "../../agents/skills.js";
|
|
6
|
+
import { loadWorkspaceSkillEntries, resolveBundledSkillsDir, } from "../../agents/skills.js";
|
|
7
7
|
import { bumpSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|
8
8
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
|
9
9
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
|
10
|
-
import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsCreateParams, validateSkillsDeleteDraftParams, validateSkillsDeleteParams, validateSkillsDraftsParams, validateSkillsInstallParams, validateSkillsReadParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
|
|
10
|
+
import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsCreateParams, validateSkillsDeleteDraftParams, validateSkillsDeleteParams, validateSkillsDraftsParams, validateSkillsInstallParams, validateSkillsReadParams, validateSkillsSaveDraftParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
|
|
11
11
|
function listWorkspaceDirs(cfg) {
|
|
12
12
|
const dirs = new Set();
|
|
13
13
|
const list = cfg.agents?.list;
|
|
@@ -56,8 +56,7 @@ function isPreloadedSkill(skillKey) {
|
|
|
56
56
|
if (!dir)
|
|
57
57
|
return false;
|
|
58
58
|
try {
|
|
59
|
-
return fs.existsSync(`${dir}/${skillKey}`) &&
|
|
60
|
-
fs.statSync(`${dir}/${skillKey}`).isDirectory();
|
|
59
|
+
return fs.existsSync(`${dir}/${skillKey}`) && fs.statSync(`${dir}/${skillKey}`).isDirectory();
|
|
61
60
|
}
|
|
62
61
|
catch {
|
|
63
62
|
return false;
|
|
@@ -274,6 +273,31 @@ export const skillsHandlers = {
|
|
|
274
273
|
}
|
|
275
274
|
respond(true, { drafts }, undefined);
|
|
276
275
|
},
|
|
276
|
+
"skills.saveDraft": ({ params, respond }) => {
|
|
277
|
+
if (!validateSkillsSaveDraftParams(params)) {
|
|
278
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.saveDraft params: ${formatValidationErrors(validateSkillsSaveDraftParams.errors)}`));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const p = params;
|
|
282
|
+
const cfg = loadConfig();
|
|
283
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
284
|
+
const draftDir = path.join(root, ".skill-drafts", p.name);
|
|
285
|
+
// Clean replace if it already exists
|
|
286
|
+
if (fs.existsSync(draftDir)) {
|
|
287
|
+
fs.rmSync(draftDir, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
fs.mkdirSync(draftDir, { recursive: true });
|
|
290
|
+
fs.writeFileSync(path.join(draftDir, "SKILL.md"), p.skillContent, "utf-8");
|
|
291
|
+
if (p.references && p.references.length > 0) {
|
|
292
|
+
const refsDir = path.join(draftDir, "references");
|
|
293
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
294
|
+
for (const ref of p.references) {
|
|
295
|
+
const safeName = ref.name.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
296
|
+
fs.writeFileSync(path.join(refsDir, safeName), ref.content, "utf-8");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
respond(true, { ok: true, name: p.name }, undefined);
|
|
300
|
+
},
|
|
277
301
|
"skills.deleteDraft": ({ params, respond }) => {
|
|
278
302
|
if (!validateSkillsDeleteDraftParams(params)) {
|
|
279
303
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.deleteDraft params: ${formatValidationErrors(validateSkillsDeleteDraftParams.errors)}`));
|
|
@@ -102,6 +102,23 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
102
102
|
.join("\n")}`);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
// Persist default model fallbacks to config if missing. The in-memory
|
|
106
|
+
// defaults cover runtime, but writing to disk makes the fallback chain
|
|
107
|
+
// visible in the config file and survives across gateway restarts.
|
|
108
|
+
if (configSnapshot.exists && !isNixMode) {
|
|
109
|
+
const parsed = configSnapshot.parsed;
|
|
110
|
+
const agentsRaw = parsed?.agents;
|
|
111
|
+
const defaultsRaw = agentsRaw?.defaults;
|
|
112
|
+
const modelRaw = defaultsRaw?.model;
|
|
113
|
+
const fallbacks = modelRaw?.fallbacks;
|
|
114
|
+
if (!Array.isArray(fallbacks) || fallbacks.length === 0) {
|
|
115
|
+
const { config: migrated, changes } = migrateLegacyConfig(parsed ?? {});
|
|
116
|
+
if (migrated && changes.length > 0) {
|
|
117
|
+
await writeConfigFile(migrated);
|
|
118
|
+
log.info(`gateway: applied config defaults:\n${changes.map((entry) => `- ${entry}`).join("\n")}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
105
122
|
configSnapshot = await readConfigFileSnapshot();
|
|
106
123
|
if (configSnapshot.exists && !configSnapshot.valid) {
|
|
107
124
|
const issues = configSnapshot.issues.length > 0
|
package/dist/memory/audit.js
CHANGED
|
@@ -15,7 +15,7 @@ import fs from "node:fs";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
const AUDIT_FILENAME = ".memory-audit.json";
|
|
17
17
|
/** Paths that are excluded from audit — expected operational writes. */
|
|
18
|
-
const EXCLUDED_PREFIXES = ["memory/shared/events/"];
|
|
18
|
+
const EXCLUDED_PREFIXES = ["memory/shared/events/", "memory/shared/cron-activity/"];
|
|
19
19
|
/**
|
|
20
20
|
* Returns true if a memory write path should be audited.
|
|
21
21
|
* Auditable paths: memory/shared/** and memory/public/** (excluding exemptions).
|
|
@@ -59,7 +59,11 @@ function writeAuditFile(workspaceDir, data) {
|
|
|
59
59
|
const DEDUP_WINDOW_MS = 60_000;
|
|
60
60
|
/**
|
|
61
61
|
* Record an audit entry for a memory write.
|
|
62
|
-
*
|
|
62
|
+
*
|
|
63
|
+
* Skips the entry if:
|
|
64
|
+
* - Same path + agent was recorded within the dedup window (rapid successive writes), OR
|
|
65
|
+
* - The most recent entry for the same path has the same content hash
|
|
66
|
+
* (re-discovery of unchanged content during a search index rebuild)
|
|
63
67
|
*/
|
|
64
68
|
export function recordAuditEntry(workspaceDir, entry) {
|
|
65
69
|
const audit = readAuditFile(workspaceDir);
|
|
@@ -69,6 +73,19 @@ export function recordAuditEntry(workspaceDir, entry) {
|
|
|
69
73
|
entry.timestamp - e.timestamp < DEDUP_WINDOW_MS);
|
|
70
74
|
if (dominated)
|
|
71
75
|
return;
|
|
76
|
+
// Content-hash dedup: skip if the most recent entry for this path has the same hash.
|
|
77
|
+
// This prevents re-alerting when the search index is rebuilt (full reindex) but
|
|
78
|
+
// the file content hasn't actually changed.
|
|
79
|
+
if (entry.hash) {
|
|
80
|
+
let latestForPath;
|
|
81
|
+
for (const e of audit.entries) {
|
|
82
|
+
if (e.path === entry.path && (!latestForPath || e.timestamp > latestForPath.timestamp)) {
|
|
83
|
+
latestForPath = e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (latestForPath?.hash === entry.hash)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
72
89
|
audit.entries.push(entry);
|
|
73
90
|
// Cap at 500 entries to prevent unbounded growth.
|
|
74
91
|
if (audit.entries.length > 500) {
|
package/dist/memory/manager.js
CHANGED
package/package.json
CHANGED
|
@@ -71,14 +71,19 @@ Show the user the complete SKILL.md content and each reference file. Ask them to
|
|
|
71
71
|
|
|
72
72
|
## Step 6: Save the Draft
|
|
73
73
|
|
|
74
|
-
Use
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
Use the `skill_draft_save` tool:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
skill_draft_save({
|
|
78
|
+
name: "{name}",
|
|
79
|
+
skill_content: "...", // full SKILL.md including frontmatter
|
|
80
|
+
references: [ // optional — omit if no reference files
|
|
81
|
+
{ name: "filename.md", content: "..." }
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The tool saves the draft to the correct location where the Control Panel looks for drafts. Do not use `memory_write` or `write` — those save to the wrong location.
|
|
82
87
|
|
|
83
88
|
## Step 7: Direct to Control Panel
|
|
84
89
|
|