brand-manager-worker 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/worker/compact.js +71 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-manager-worker",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The Goose Tools brand-deal worker \u2014 your computer reads your brand email and drafts replies in your voice for goosetools.com, using your own Claude account. Drafts only; it never sends.",
5
5
  "repository": {
6
6
  "type": "git",
package/worker/compact.js CHANGED
@@ -37,6 +37,48 @@ export const COMPACTABLE = [
37
37
 
38
38
  const ARCHIVE_DIR = join(BRAND_DIR, "archive");
39
39
 
40
+ /**
41
+ * A free filename. The stamp used to be the date alone, so a second
42
+ * compaction on the same day overwrote the first archive — replacing the true
43
+ * original with an already-compacted copy and quietly destroying the only
44
+ * record of what was dropped.
45
+ */
46
+ function archivePathFor(target) {
47
+ const stamp = new Date().toISOString().slice(0, 10);
48
+ let candidate = join(ARCHIVE_DIR, `${target}-${stamp}.md`);
49
+ for (let n = 2; existsSync(candidate); n += 1) {
50
+ candidate = join(ARCHIVE_DIR, `${target}-${stamp}-${n}.md`);
51
+ }
52
+ return candidate;
53
+ }
54
+
55
+ // Compaction can land a file just above the threshold — negotiation.md came
56
+ // out at 48KB against a 40KB line. Without a memory of that, every scan
57
+ // forever would re-compact it: an agent call each time, for a file that is
58
+ // already as small as it gets. So a file is left alone after a compaction
59
+ // until it has actually grown again.
60
+ const STATE_PATH = () => join(ARCHIVE_DIR, ".compact-state.json");
61
+ const REGROWTH = 1.15;
62
+
63
+ function readState() {
64
+ try {
65
+ return JSON.parse(readFileSync(STATE_PATH(), "utf8"));
66
+ } catch {
67
+ return {};
68
+ }
69
+ }
70
+
71
+ function noteCompaction(target, size) {
72
+ try {
73
+ const state = readState();
74
+ state[target] = { size, at: new Date().toISOString() };
75
+ mkdirSync(ARCHIVE_DIR, { recursive: true });
76
+ writeFileSync(STATE_PATH(), JSON.stringify(state, null, 2));
77
+ } catch {
78
+ // A missing note only costs one redundant compaction; never fail over it.
79
+ }
80
+ }
81
+
40
82
  export function sizeOf(path) {
41
83
  try {
42
84
  return statSync(path).size;
@@ -46,7 +88,14 @@ export function sizeOf(path) {
46
88
  }
47
89
 
48
90
  export function oversized() {
49
- return COMPACTABLE.filter(({ path }) => sizeOf(path) > COMPACT_THRESHOLD);
91
+ const state = readState();
92
+ return COMPACTABLE.filter(({ target, path }) => {
93
+ const size = sizeOf(path);
94
+ if (size <= COMPACT_THRESHOLD) return false;
95
+ const last = state[target];
96
+ // Already squeezed and hasn't meaningfully regrown — nothing left to win.
97
+ return !last?.size || size > last.size * REGROWTH;
98
+ });
50
99
  }
51
100
 
52
101
  const prompt = (label, body) =>
@@ -100,33 +149,36 @@ export async function compactFile({ target, path }) {
100
149
  );
101
150
  const after = typeof out?.markdown === "string" ? out.markdown.trim() : "";
102
151
 
152
+ // A transient failure (bad output, timeout) must stay retryable — the file
153
+ // is over budget and we want another go. Only the deterministic refusals
154
+ // below get remembered, because re-asking would refuse identically.
103
155
  if (!after) return { target, ok: false, error: "no parseable rewrite" };
104
156
 
105
157
  // Guards. An agent asked to shrink a file can decide to summarize it, and a
106
158
  // 3KB precis of 66KB of learned voice is a catastrophic, silent loss — the
107
159
  // archive would be the only copy and nobody would notice for weeks.
108
160
  if (after.length < before.length * 0.25) {
109
- return {
161
+ return refuse(
110
162
  target,
111
- ok: false,
112
- error: `refused: ${before.length} -> ${after.length} bytes looks like a summary, not a rewrite`,
113
- };
163
+ before.length,
164
+ `${before.length} -> ${after.length} bytes looks like a summary, not a rewrite`,
165
+ );
114
166
  }
115
167
  if (after.length >= before.length) {
116
- return { target, ok: false, error: "refused: no smaller than the original" };
168
+ return refuse(target, before.length, "no smaller than the original");
117
169
  }
118
170
  // The headings are the file's skeleton; losing most of them means it was
119
171
  // restructured rather than compacted.
120
172
  const headingsBefore = (before.match(/^##\s/gm) ?? []).length;
121
173
  const headingsAfter = (after.match(/^##\s/gm) ?? []).length;
122
174
  if (headingsBefore >= 4 && headingsAfter < 2) {
123
- return { target, ok: false, error: "refused: lost the section structure" };
175
+ return refuse(target, before.length, "lost the section structure");
124
176
  }
125
177
 
126
178
  mkdirSync(ARCHIVE_DIR, { recursive: true });
127
- const stamp = new Date().toISOString().slice(0, 10);
128
- writeFileSync(join(ARCHIVE_DIR, `${target}-${stamp}.md`), before);
179
+ writeFileSync(archivePathFor(target), before);
129
180
  writeFileSync(path, `${after}\n`);
181
+ noteCompaction(target, after.length);
130
182
 
131
183
  return {
132
184
  target,
@@ -139,6 +191,16 @@ export async function compactFile({ target, path }) {
139
191
  };
140
192
  }
141
193
 
194
+ /**
195
+ * A refusal the agent would repeat verbatim if asked again. Remember the size
196
+ * so the file isn't re-attempted every scan — it becomes eligible again only
197
+ * once it has genuinely regrown.
198
+ */
199
+ function refuse(target, size, why) {
200
+ noteCompaction(target, size);
201
+ return { target, ok: false, error: `refused: ${why}` };
202
+ }
203
+
142
204
  /**
143
205
  * Compact anything over the threshold. Called at the END of a scan, once —
144
206
  * not per thread, so a scan pays for at most one of these and only on the day