@agentprojectcontext/apx 1.80.0 → 1.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.80.0",
3
+ "version": "1.80.1",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,79 @@
1
+ import { appendRoutineMemory } from "#core/stores/routine-memory.js";
2
+
3
+ /**
4
+ * A routine writing to its OWN memory.
5
+ *
6
+ * WHY THIS TOOL EXISTS. Routine memory is a file, and until now the only way to
7
+ * write it was `write_file` — which is gated as dangerous, correctly, because it
8
+ * can write anywhere. In a routine there is nobody to confirm anything, so
9
+ * `requirePermission` threw "Action requires user confirmation", the model
10
+ * treated it as a dead end, and the evening anchor ended without sending
11
+ * anything. The run still reported `ok`, so the only symptom was silence.
12
+ *
13
+ * An agent recording what it learned is not a dangerous act. It is the most
14
+ * ordinary thing it does, and it must not need a human standing by.
15
+ *
16
+ * WHY IT IS SAFE UNGATED: there is no path argument. The destination comes from
17
+ * the running routine's own context (channelMeta.routineId + the project's
18
+ * storage), so this tool cannot write anywhere else however it is called. That
19
+ * is the whole difference between this and write_file, and it is the reason the
20
+ * permission is unnecessary rather than merely inconvenient.
21
+ */
22
+ export default {
23
+ name: "remember_routine",
24
+ schema: {
25
+ type: "function",
26
+ function: {
27
+ name: "remember_routine",
28
+ description:
29
+ "Save a durable note to THIS routine's own memory — what you learned about how the " +
30
+ "owner works, what turned out to be worth mentioning, what turned out to be noise. " +
31
+ "Only available while a routine is running. Use this instead of write_file: it is the " +
32
+ "routine's own notebook and needs no permission. For facts about the owner that matter " +
33
+ "on every channel, use `remember` instead.",
34
+ parameters: {
35
+ type: "object",
36
+ required: ["note"],
37
+ properties: {
38
+ note: {
39
+ type: "string",
40
+ description: "One line. A judgement that will change what you do next time, not a log of what happened.",
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ makeHandler: ({ channel, channelMeta, projects }) => async ({ note } = {}) => {
47
+ const text = String(note || "").trim();
48
+ if (!text) return { error: "note required" };
49
+
50
+ const routineId = channelMeta?.routineId;
51
+ const routineName = channelMeta?.routineName || "";
52
+ if (!routineId) {
53
+ // Said plainly so the model reaches for `remember` instead of retrying.
54
+ return {
55
+ error:
56
+ "remember_routine only works inside a running routine. " +
57
+ "Use `remember` for a durable fact about the owner.",
58
+ channel: channel || null,
59
+ };
60
+ }
61
+
62
+ // The routine's project storage, never a model-supplied path.
63
+ const projectPath = channelMeta?.projectPath || "";
64
+ let storagePath = "";
65
+ for (const entry of projects?.list?.() || []) {
66
+ if (projectPath && entry.path !== projectPath) continue;
67
+ storagePath = projects.get(entry.id)?.storagePath || "";
68
+ if (storagePath) break;
69
+ }
70
+ if (!storagePath) return { error: "could not resolve this routine's storage" };
71
+
72
+ try {
73
+ const r = appendRoutineMemory(storagePath, routineId, text, { routineName });
74
+ return { saved: true, routine: routineName || routineId, note: text, path: r?.path };
75
+ } catch (e) {
76
+ return { error: e.message };
77
+ }
78
+ },
79
+ };
@@ -42,6 +42,7 @@ export const TOOLS = Object.freeze({
42
42
  LIST_TASKS: "list_tasks",
43
43
  CREATE_TASK: "create_task",
44
44
  RECORD_COMMITMENT: "record_commitment",
45
+ REMEMBER_ROUTINE: "remember_routine",
45
46
  LIST_COMMITMENTS: "list_commitments",
46
47
 
47
48
  // Interaction
@@ -139,6 +140,7 @@ export const NATIVE_TOOL_NAMES = new Set([
139
140
  TOOLS.CREATE_TASK,
140
141
  TOOLS.RECORD_COMMITMENT,
141
142
  TOOLS.LIST_COMMITMENTS,
143
+ TOOLS.REMEMBER_ROUTINE,
142
144
  TOOLS.ASK_QUESTIONS,
143
145
  TOOLS.SEARCH_SESSIONS,
144
146
  TOOLS.TRANSCRIBE_AUDIO,
@@ -228,6 +230,7 @@ export const SIDE_EFFECT_TOOLS = new Set([
228
230
  TOOLS.SEND_TELEGRAM,
229
231
  TOOLS.CREATE_TASK,
230
232
  TOOLS.RECORD_COMMITMENT,
233
+ TOOLS.REMEMBER_ROUTINE,
231
234
  TOOLS.WRITE_FILE,
232
235
  TOOLS.EDIT_FILE,
233
236
  TOOLS.RUN_SHELL,
@@ -30,6 +30,7 @@ import askQuestions from "./handlers/ask-questions.js";
30
30
  import createTask from "./handlers/create-task.js";
31
31
  import recordCommitment from "./handlers/record-commitment.js";
32
32
  import listCommitments from "./handlers/list-commitments.js";
33
+ import rememberRoutine from "./handlers/remember-routine.js";
33
34
  import listTasks from "./handlers/list-tasks.js";
34
35
  import discoverTools from "./handlers/discover-tools.js";
35
36
  import gitStatus from "./handlers/git-status.js";
@@ -85,6 +86,7 @@ const NATIVE_TOOLS = [
85
86
  listTasks,
86
87
  recordCommitment,
87
88
  listCommitments,
89
+ rememberRoutine,
88
90
  discoverTools,
89
91
  gitStatus,
90
92
  gitDiff,
@@ -143,6 +145,10 @@ export const BASE_TOOL_NAMES = new Set([
143
145
  TOOLS.READ_AGENT_MEMORY,
144
146
  TOOLS.READ_SELF_MEMORY,
145
147
  TOOLS.REMEMBER,
148
+ // NOT here: remember_routine. It only works inside a routine, and the routine
149
+ // channel is in FULL_CHANNELS — it already receives the whole registry. Adding
150
+ // it to the base set would spend tokens on every Telegram and desktop turn for
151
+ // a tool that returns an error on those channels.
146
152
  TOOLS.SET_IDENTITY,
147
153
  // Sessions + messages (self-recall + channel history).
148
154
  TOOLS.SEARCH_SESSIONS,
@@ -5,7 +5,7 @@
5
5
  "permission_mode": "permiso",
6
6
  "enabled_by_default": true,
7
7
  "spec": {
8
- "prompt": "Close the day. Review what moved today, what is still stuck, and what carries over to tomorrow, across every registered project. Call list_tasks and list_commitments ONCE each, with no project argument — both search every project in a single call. Do not loop over projects. If a commitment came due today, ask whether it was kept — do not mark it yourself.\n\nSend ONE short message covering those, keeping promises to people separate from ordinary tasks. Note anything that has gone quiet longer than the staleness threshold. If the day was quiet, say that plainly in a line or two.\n\nThen record in your routine memory anything durable you learned about how this person works.\n\nDELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. Send exactly one message, then stop; do not end the turn by asking whether to keep looking.",
8
+ "prompt": "Close the day. Review what moved today, what is still stuck, and what carries over to tomorrow, across every registered project. Call list_tasks and list_commitments ONCE each, with no project argument — both search every project in a single call. Do not loop over projects. If a commitment came due today, ask whether it was kept — do not mark it yourself.\n\nSend ONE short message covering those, keeping promises to people separate from ordinary tasks. Note anything that has gone quiet longer than the staleness threshold. If the day was quiet, say that plainly in a line or two.\n\nThen record anything durable you learned about how this person works with remember_routine — one line. Never write_file for this: a scheduled run has nobody to approve it and the whole turn dies there.\n\nDELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. Send exactly one message, then stop; do not end the turn by asking whether to keep looking.",
9
9
  "anchor": true
10
10
  },
11
11
  "allowed_tools": [
@@ -5,7 +5,7 @@
5
5
  "permission_mode": "permiso",
6
6
  "enabled_by_default": true,
7
7
  "spec": {
8
- "prompt": "You are watching {{owner_name}}'s projects between the anchors. The signals below were detected deterministically — they are facts, already gathered; you do not need to look them up again.\n\nDecide whether any of it is worth interrupting for RIGHT NOW rather than waiting for the next anchor. Staying quiet is the normal outcome and costs you nothing. A message that could have waited until tomorrow morning costs you the next one, because that is the one they will not open.\n\nIf you do speak: send ONE line naming the single most important thing and what you suggest doing about it, with send_telegram. DELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. If you decide to stay quiet, just say so in your reply and call nothing."
8
+ "prompt": "You are watching {{owner_name}}'s projects between the anchors. The signals below were detected deterministically — they are facts, already gathered; you do not need to look them up again.\n\nDecide whether any of it is worth interrupting for RIGHT NOW rather than waiting for the next anchor. Staying quiet is the normal outcome and costs you nothing. A message that could have waited until tomorrow morning costs you the next one, because that is the one they will not open.\n\nIf you do speak: send ONE line naming the single most important thing and what you suggest doing about it, with send_telegram. DELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. If you decide to stay quiet, just say so in your reply and call nothing.\n\nWhichever you choose, note what you learned with remember_routine — especially when you stayed quiet, because next time you need to know this kind of signal was not worth it."
9
9
  },
10
10
  "allowed_tools": [
11
11
  "send_telegram"
@@ -145,6 +145,12 @@ async function handleSuperAgent(ctx, routine, extraChannelMeta = {}) {
145
145
  suppressTools: suppressTools.length > 0 ? suppressTools : null,
146
146
  });
147
147
 
148
+ // A tool that needed a human in a run with no human is a DEAD END, not a
149
+ // hiccup: nobody is ever going to confirm it. Reporting the run as "ok"
150
+ // meant the only symptom was silence — the evening anchor produced nothing
151
+ // and it took twenty-one shell commands to work out why. Name it.
152
+ const blocked = blockedForPermission(result.trace);
153
+
148
154
  project.logMessage({
149
155
  channel: CHANNELS.ROUTINE,
150
156
  direction: "out",
@@ -161,9 +167,37 @@ async function handleSuperAgent(ctx, routine, extraChannelMeta = {}) {
161
167
  usage: result.usage,
162
168
  },
163
169
  });
170
+ if (blocked.length) {
171
+ return {
172
+ status: "error",
173
+ error:
174
+ `blocked waiting for a confirmation nobody can give: ${blocked.join(", ")}. ` +
175
+ `A scheduled run has no one to approve a dangerous tool — either allow it on ` +
176
+ `this routine (allowed_tools) or use a tool that does not need approval.`,
177
+ blocked_tools: blocked,
178
+ reply: result.text,
179
+ trace: result.trace,
180
+ };
181
+ }
164
182
  return { status: "ok", reply: result.text, trace: result.trace };
165
183
  }
166
184
 
185
+ /**
186
+ * Tools whose result was "Action requires user confirmation" — the message
187
+ * createPermissionGuard throws when there is no confirmation channel wired
188
+ * (tools/helpers.js). Distinct from a tool that merely failed.
189
+ */
190
+ export function blockedForPermission(trace) {
191
+ const names = new Set();
192
+ for (const item of Array.isArray(trace) ? trace : []) {
193
+ const err = item?.result?.error;
194
+ if (typeof err === "string" && /requires user confirmation/i.test(err)) {
195
+ names.add(item.tool || "unknown");
196
+ }
197
+ }
198
+ return [...names];
199
+ }
200
+
167
201
  async function handleTelegram(ctx, routine) {
168
202
  const { plugins } = ctx;
169
203
  const tg = plugins?.get("telegram");