@agentprojectcontext/apx 1.74.1 → 1.74.2

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.74.1",
3
+ "version": "1.74.2",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { CronExpressionParser } from "cron-parser";
6
6
  import { nowIso, isoToMs } from "../util/time.js";
7
+ import { shortId } from "../util/ids.js";
7
8
 
8
9
  function routinesPath(storagePath) {
9
10
  // storagePath is always ~/.apx/projects/{apxId}/ — flat, no .apc subdir needed.
@@ -82,13 +83,58 @@ export function computeNextRun(routine, baseMs = Date.now()) {
82
83
  return null;
83
84
  }
84
85
 
86
+ // --------------------- ids + migration --------------------------------------
87
+
88
+ // Routines are addressed by `name` everywhere (getRoutine/deleteRoutine/
89
+ // setEnabled/updateRunState), but per-routine memory is keyed by `id`
90
+ // (stores/routine-memory.js). Records written before ids existed have none, so
91
+ // every one of them resolved to the shared `routines/_unknown/memory.md`.
92
+ //
93
+ // This migration is deliberately NOT inside readFile(): that helper is on the
94
+ // scheduler's 5s polling path, and writing from it would mean write
95
+ // amplification plus a read-modify-write race against a concurrent CLI edit.
96
+ // Instead the public read entry points call this, and it early-returns without
97
+ // touching disk once every record has an id — so the write happens once per
98
+ // project, ever.
99
+
100
+ /**
101
+ * Backfill `id` on any routine record that predates the field.
102
+ * Returns the number of records migrated (0 when there was nothing to do).
103
+ */
104
+ export function ensureRoutineIds(storagePath) {
105
+ const routines = readFile(storagePath);
106
+ const missing = routines.filter((r) => r && !r.id);
107
+ if (missing.length === 0) return 0;
108
+
109
+ for (const r of missing) r.id = shortId("r");
110
+ writeFile(storagePath, routines);
111
+
112
+ // Anything already written under routines/_unknown/ belonged to an
113
+ // indeterminate set of routines — we can't know which, so we leave it where
114
+ // it is rather than guess, and say so out loud once.
115
+ const orphan = path.join(storagePath, "routines", "_unknown");
116
+ if (fs.existsSync(orphan)) {
117
+ // eslint-disable-next-line no-console
118
+ console.warn(
119
+ `[apx] routines: assigned ids to ${missing.length} routine(s). Pre-existing shared\n` +
120
+ ` memory at ${orphan} was left untouched — it cannot be attributed to a single\n` +
121
+ ` routine. Copy anything worth keeping into the per-routine memory files.`
122
+ );
123
+ }
124
+ return missing.length;
125
+ }
126
+
85
127
  // --------------------- CRUD -------------------------------------------------
86
128
 
87
129
  export function listRoutines(projectPath) {
130
+ ensureRoutineIds(projectPath);
88
131
  return readFile(projectPath);
89
132
  }
90
133
 
91
134
  export function getRoutine(projectPath, name) {
135
+ // Callers hand the record straight to the runner, which needs `id` for
136
+ // per-routine memory.
137
+ ensureRoutineIds(projectPath);
92
138
  return readFile(projectPath).find((r) => r.name === name) || null;
93
139
  }
94
140
 
@@ -100,6 +146,10 @@ export function upsertRoutine(storagePath, { name, kind, schedule, spec, enabled
100
146
  const prev = idx >= 0 ? routines[idx] : null;
101
147
  const next = computeNextRun({ schedule, last_run_at: null });
102
148
  const entry = {
149
+ // `entry` is rebuilt from scratch on every upsert, so the id MUST be
150
+ // carried over explicitly (same as created_at below). Dropping it here
151
+ // would re-id the routine on every edit and orphan its memory directory.
152
+ id: prev?.id || shortId("r"),
103
153
  name,
104
154
  kind,
105
155
  schedule,
@@ -167,6 +217,8 @@ export function updateRunState(projectPath, name, { last_run_at, last_status, la
167
217
  }
168
218
 
169
219
  export function getDueRoutines(projectPath, nowStr) {
220
+ // The runner keys per-routine memory off `id`, so due records must carry one.
221
+ ensureRoutineIds(projectPath);
170
222
  return readFile(projectPath).filter((r) => {
171
223
  if (!r.enabled) return false;
172
224
  // CRITICAL: If the schedule cannot be parsed, NEVER run it.