@massu/core 1.15.7 → 1.16.0
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/CHANGELOG.md +63 -0
- package/dist/cli.js +1943 -774
- package/dist/hooks/auto-learning-pipeline.js +152 -67
- package/dist/hooks/classify-failure.js +152 -67
- package/dist/hooks/cost-tracker.js +152 -67
- package/dist/hooks/fix-detector.js +152 -67
- package/dist/hooks/incident-pipeline.js +152 -67
- package/dist/hooks/intent-suggester.js +152 -67
- package/dist/hooks/memory-recall.js +152 -67
- package/dist/hooks/post-edit-context.js +152 -67
- package/dist/hooks/post-tool-use.js +152 -67
- package/dist/hooks/pre-compact.js +152 -67
- package/dist/hooks/pre-delete-check.js +154 -69
- package/dist/hooks/pre-tool-use-gate.js +154 -69
- package/dist/hooks/quality-event.js +152 -67
- package/dist/hooks/rule-enforcement-pipeline.js +152 -67
- package/dist/hooks/security-gate.js +152 -67
- package/dist/hooks/session-end.js +346 -80
- package/dist/hooks/session-start.js +155 -67
- package/dist/hooks/user-prompt.js +222 -111
- package/package.json +1 -1
|
@@ -971,6 +971,253 @@ var init_db_backup = __esm({
|
|
|
971
971
|
}
|
|
972
972
|
});
|
|
973
973
|
|
|
974
|
+
// src/rule-delivery.ts
|
|
975
|
+
import { randomUUID } from "node:crypto";
|
|
976
|
+
function tableExists(db, table) {
|
|
977
|
+
const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name=?").get(table);
|
|
978
|
+
return row !== void 0;
|
|
979
|
+
}
|
|
980
|
+
function claimPage(db, table, pk, orderBy, limit, token, nowMs) {
|
|
981
|
+
if (!tableExists(db, table)) return;
|
|
982
|
+
const cutoff = nowMs - LEASE_TTL_MS;
|
|
983
|
+
const ids = db.prepare(
|
|
984
|
+
`SELECT ${pk} AS k FROM ${table}
|
|
985
|
+
WHERE lease_token IS NULL OR leased_at_ms IS NULL OR leased_at_ms < ?
|
|
986
|
+
ORDER BY ${orderBy} ASC LIMIT ?`
|
|
987
|
+
).all(cutoff, limit);
|
|
988
|
+
if (ids.length === 0) return;
|
|
989
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
990
|
+
db.prepare(
|
|
991
|
+
`UPDATE ${table} SET lease_token = ?, leased_at_ms = ? WHERE ${pk} IN (${placeholders})`
|
|
992
|
+
).run(token, nowMs, ...ids.map((r) => r.k));
|
|
993
|
+
}
|
|
994
|
+
function columns(db, table) {
|
|
995
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
996
|
+
return new Set(rows.map((r) => r.name));
|
|
997
|
+
}
|
|
998
|
+
function migrateRuleDelivery(db) {
|
|
999
|
+
const ADDITIONS = [
|
|
1000
|
+
{ name: "attempts", decl: "INTEGER NOT NULL DEFAULT 0" },
|
|
1001
|
+
{ name: "lease_token", decl: "TEXT" },
|
|
1002
|
+
{ name: "leased_at_ms", decl: "INTEGER" },
|
|
1003
|
+
{ name: "last_error", decl: "TEXT" }
|
|
1004
|
+
];
|
|
1005
|
+
for (const table of ALL_OUTBOUND_TABLES) {
|
|
1006
|
+
if (!tableExists(db, table)) continue;
|
|
1007
|
+
const cols = columns(db, table);
|
|
1008
|
+
for (const add of ADDITIONS) {
|
|
1009
|
+
if (cols.has(add.name)) continue;
|
|
1010
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function leaseLearning(db, nowMs = Date.now()) {
|
|
1015
|
+
migrateRuleDelivery(db);
|
|
1016
|
+
const token = randomUUID();
|
|
1017
|
+
claimPage(db, "team_promotion_outbound", "prompt_hash", "created_at", PROMOTION_LEASE_LIMIT, token, nowMs);
|
|
1018
|
+
claimPage(db, "team_revocation_outbound", "prompt_hash", "created_at", PROMOTION_LEASE_LIMIT, token, nowMs);
|
|
1019
|
+
claimPage(db, TELEMETRY_OUTBOUND_TABLE, "id", "id", TELEMETRY_LEASE_LIMIT, token, nowMs);
|
|
1020
|
+
const promotions = tableExists(db, "team_promotion_outbound") ? db.prepare(
|
|
1021
|
+
`SELECT prompt_hash, destination, draft_text, score, signals_json,
|
|
1022
|
+
content_hash, hardened, review_attestation_json
|
|
1023
|
+
FROM team_promotion_outbound WHERE lease_token = ?
|
|
1024
|
+
ORDER BY created_at ASC LIMIT ?`
|
|
1025
|
+
).all(token, PROMOTION_LEASE_LIMIT).map((r) => ({
|
|
1026
|
+
prompt_hash: r.prompt_hash,
|
|
1027
|
+
destination: r.destination,
|
|
1028
|
+
draft_text: r.draft_text,
|
|
1029
|
+
...r.score !== null ? { score: r.score } : {},
|
|
1030
|
+
signals: safeParseArray(r.signals_json),
|
|
1031
|
+
content_hash: r.content_hash,
|
|
1032
|
+
...r.hardened ? { hardened: true } : {},
|
|
1033
|
+
...r.review_attestation_json != null ? { review_attestation: safeParseUnknown(r.review_attestation_json) } : {}
|
|
1034
|
+
})) : [];
|
|
1035
|
+
const revocations = tableExists(db, "team_revocation_outbound") ? db.prepare(
|
|
1036
|
+
`SELECT prompt_hash FROM team_revocation_outbound
|
|
1037
|
+
WHERE lease_token = ? ORDER BY created_at ASC LIMIT ?`
|
|
1038
|
+
).all(token, PROMOTION_LEASE_LIMIT).map((r) => r.prompt_hash) : [];
|
|
1039
|
+
const events = tableExists(db, TELEMETRY_OUTBOUND_TABLE) ? db.prepare(
|
|
1040
|
+
`SELECT prompt_hash, event_type, metadata_json, created_at
|
|
1041
|
+
FROM ${TELEMETRY_OUTBOUND_TABLE} WHERE lease_token = ?
|
|
1042
|
+
ORDER BY id ASC LIMIT ?`
|
|
1043
|
+
).all(token, TELEMETRY_LEASE_LIMIT).map((r) => ({
|
|
1044
|
+
prompt_hash: r.prompt_hash,
|
|
1045
|
+
event_type: r.event_type,
|
|
1046
|
+
created_at: r.created_at,
|
|
1047
|
+
metadata: safeParseUnknown(r.metadata_json) ?? {}
|
|
1048
|
+
})) : [];
|
|
1049
|
+
return { token, promotions, revocations, events };
|
|
1050
|
+
}
|
|
1051
|
+
function ackLearning(db, lease) {
|
|
1052
|
+
const delivered = lease.promotions.length + lease.revocations.length + lease.events.length;
|
|
1053
|
+
for (const table of ALL_OUTBOUND_TABLES) {
|
|
1054
|
+
if (!tableExists(db, table)) continue;
|
|
1055
|
+
db.prepare(`DELETE FROM ${table} WHERE lease_token = ?`).run(lease.token);
|
|
1056
|
+
}
|
|
1057
|
+
if (delivered > 0) {
|
|
1058
|
+
recordAnalytics(db, EVENT_DELIVERY_CONFIRMED, {
|
|
1059
|
+
promotions: lease.promotions.length,
|
|
1060
|
+
revocations: lease.revocations.length,
|
|
1061
|
+
events: lease.events.length
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function nackLearning(db, lease, error) {
|
|
1066
|
+
for (const table of ALL_OUTBOUND_TABLES) {
|
|
1067
|
+
if (!tableExists(db, table)) continue;
|
|
1068
|
+
db.prepare(
|
|
1069
|
+
`UPDATE ${table}
|
|
1070
|
+
SET lease_token = NULL,
|
|
1071
|
+
leased_at_ms = NULL,
|
|
1072
|
+
attempts = attempts + 1,
|
|
1073
|
+
last_error = ?
|
|
1074
|
+
WHERE lease_token = ?`
|
|
1075
|
+
).run(error.slice(0, 500), lease.token);
|
|
1076
|
+
}
|
|
1077
|
+
const snap = undeliveredSnapshot(db);
|
|
1078
|
+
if (snap.stalled) alarmStalled(db, snap, error);
|
|
1079
|
+
return snap;
|
|
1080
|
+
}
|
|
1081
|
+
function undeliveredSnapshot(db) {
|
|
1082
|
+
const count = (t) => {
|
|
1083
|
+
if (!tableExists(db, t)) return 0;
|
|
1084
|
+
const r = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
|
|
1085
|
+
return r.n;
|
|
1086
|
+
};
|
|
1087
|
+
const promotions = count("team_promotion_outbound");
|
|
1088
|
+
const revocations = count("team_revocation_outbound");
|
|
1089
|
+
const events = count(TELEMETRY_OUTBOUND_TABLE);
|
|
1090
|
+
let maxAttempts = 0;
|
|
1091
|
+
let oldest = null;
|
|
1092
|
+
for (const t of RULE_OUTBOUND_TABLES) {
|
|
1093
|
+
if (!tableExists(db, t)) continue;
|
|
1094
|
+
if (!columns(db, t).has("attempts")) continue;
|
|
1095
|
+
const r = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t}`).get();
|
|
1096
|
+
if (r.a !== null && r.a > maxAttempts) maxAttempts = r.a;
|
|
1097
|
+
if (r.c !== null && (oldest === null || r.c < oldest)) oldest = r.c;
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
promotions,
|
|
1101
|
+
revocations,
|
|
1102
|
+
events,
|
|
1103
|
+
max_rule_attempts: maxAttempts,
|
|
1104
|
+
oldest_rule_created_at: oldest,
|
|
1105
|
+
stalled: promotions + revocations > 0 && maxAttempts >= STALL_ATTEMPTS
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function alarmStalled(db, snap, error) {
|
|
1109
|
+
const rules = snap.promotions + snap.revocations;
|
|
1110
|
+
process.stderr.write(
|
|
1111
|
+
`[massu] WARNING: ${rules} learned rule(s) have NOT reached your team after ${snap.max_rule_attempts} delivery attempts. They are SAFE (nothing was deleted) and will retry. Last error: ${error}. Run \`massu rule delivery-status\` for detail.
|
|
1112
|
+
`
|
|
1113
|
+
);
|
|
1114
|
+
recordAnalytics(db, EVENT_DELIVERY_STALLED, {
|
|
1115
|
+
undelivered_promotions: snap.promotions,
|
|
1116
|
+
undelivered_revocations: snap.revocations,
|
|
1117
|
+
undelivered_events: snap.events,
|
|
1118
|
+
max_attempts: snap.max_rule_attempts,
|
|
1119
|
+
oldest_created_at: snap.oldest_rule_created_at,
|
|
1120
|
+
last_error: error.slice(0, 300)
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function capTelemetry(db, cap = TELEMETRY_OUTBOX_CAP) {
|
|
1124
|
+
if (!tableExists(db, TELEMETRY_OUTBOUND_TABLE)) return 0;
|
|
1125
|
+
const before = db.prepare(`SELECT COUNT(*) AS n FROM ${TELEMETRY_OUTBOUND_TABLE}`).get();
|
|
1126
|
+
if (before.n <= cap) return 0;
|
|
1127
|
+
const info = db.prepare(
|
|
1128
|
+
`DELETE FROM ${TELEMETRY_OUTBOUND_TABLE}
|
|
1129
|
+
WHERE id NOT IN (SELECT id FROM ${TELEMETRY_OUTBOUND_TABLE} ORDER BY id DESC LIMIT ?)`
|
|
1130
|
+
).run(cap);
|
|
1131
|
+
const dropped = info.changes;
|
|
1132
|
+
if (dropped > 0) {
|
|
1133
|
+
process.stderr.write(
|
|
1134
|
+
`[massu] NOTE: dropped ${dropped} oldest promotion-funnel telemetry row(s) at the ${cap}-row cap. No learned rules were affected (rules are never dropped).
|
|
1135
|
+
`
|
|
1136
|
+
);
|
|
1137
|
+
recordAnalytics(db, EVENT_TELEMETRY_CAPPED, { dropped, cap });
|
|
1138
|
+
}
|
|
1139
|
+
return dropped;
|
|
1140
|
+
}
|
|
1141
|
+
function stripLearningFromPayload(payload) {
|
|
1142
|
+
const clone = { ...payload };
|
|
1143
|
+
delete clone.rule_promotions;
|
|
1144
|
+
delete clone.rule_revocations;
|
|
1145
|
+
delete clone.rule_promotion_events;
|
|
1146
|
+
return clone;
|
|
1147
|
+
}
|
|
1148
|
+
function safeParseArray(raw) {
|
|
1149
|
+
const v = safeParseUnknown(raw);
|
|
1150
|
+
return Array.isArray(v) ? v : [];
|
|
1151
|
+
}
|
|
1152
|
+
function safeParseUnknown(raw) {
|
|
1153
|
+
try {
|
|
1154
|
+
return JSON.parse(raw);
|
|
1155
|
+
} catch {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function recordAnalytics(db, eventType, data) {
|
|
1160
|
+
try {
|
|
1161
|
+
db.prepare(
|
|
1162
|
+
`INSERT INTO analytics_events (event_type, event_data, created_at)
|
|
1163
|
+
VALUES (?, ?, datetime('now'))`
|
|
1164
|
+
).run(eventType, JSON.stringify(data));
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
process.stderr.write(
|
|
1167
|
+
`[massu] WARNING: could not record '${eventType}' telemetry: ${err instanceof Error ? err.message : String(err)}
|
|
1168
|
+
`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
var RULE_OUTBOUND_TABLES, TELEMETRY_OUTBOUND_TABLE, ALL_OUTBOUND_TABLES, TELEMETRY_OUTBOX_CAP, LEASE_TTL_MS, PROMOTION_LEASE_LIMIT, TELEMETRY_LEASE_LIMIT, STALL_ATTEMPTS, EVENT_DELIVERY_STALLED, EVENT_DELIVERY_CONFIRMED, EVENT_TELEMETRY_CAPPED;
|
|
1173
|
+
var init_rule_delivery = __esm({
|
|
1174
|
+
"src/rule-delivery.ts"() {
|
|
1175
|
+
"use strict";
|
|
1176
|
+
RULE_OUTBOUND_TABLES = [
|
|
1177
|
+
"team_promotion_outbound",
|
|
1178
|
+
"team_revocation_outbound"
|
|
1179
|
+
];
|
|
1180
|
+
TELEMETRY_OUTBOUND_TABLE = "rule_promotion_events_outbound";
|
|
1181
|
+
ALL_OUTBOUND_TABLES = [
|
|
1182
|
+
...RULE_OUTBOUND_TABLES,
|
|
1183
|
+
TELEMETRY_OUTBOUND_TABLE
|
|
1184
|
+
];
|
|
1185
|
+
TELEMETRY_OUTBOX_CAP = 2e4;
|
|
1186
|
+
LEASE_TTL_MS = 15 * 60 * 1e3;
|
|
1187
|
+
PROMOTION_LEASE_LIMIT = 1e3;
|
|
1188
|
+
TELEMETRY_LEASE_LIMIT = 5e3;
|
|
1189
|
+
STALL_ATTEMPTS = 5;
|
|
1190
|
+
EVENT_DELIVERY_STALLED = "rule_delivery_stalled";
|
|
1191
|
+
EVENT_DELIVERY_CONFIRMED = "rule_delivery_confirmed";
|
|
1192
|
+
EVENT_TELEMETRY_CAPPED = "rule_telemetry_capped";
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// src/rule-candidate-store.ts
|
|
1197
|
+
function ensureRuleCandidatesTable(db) {
|
|
1198
|
+
db.exec(`
|
|
1199
|
+
CREATE TABLE IF NOT EXISTS rule_candidates (
|
|
1200
|
+
prompt_hash TEXT PRIMARY KEY,
|
|
1201
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
1202
|
+
CHECK (status IN ('proposed','shown','promoted','dismissed')),
|
|
1203
|
+
origin TEXT NOT NULL DEFAULT 'local'
|
|
1204
|
+
CHECK (origin IN ('local','team','pack')),
|
|
1205
|
+
score REAL,
|
|
1206
|
+
destination TEXT,
|
|
1207
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
1208
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1209
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1210
|
+
);
|
|
1211
|
+
CREATE INDEX IF NOT EXISTS idx_rule_candidates_status ON rule_candidates(status);
|
|
1212
|
+
CREATE INDEX IF NOT EXISTS idx_rule_candidates_created ON rule_candidates(created_at);
|
|
1213
|
+
`);
|
|
1214
|
+
}
|
|
1215
|
+
var init_rule_candidate_store = __esm({
|
|
1216
|
+
"src/rule-candidate-store.ts"() {
|
|
1217
|
+
"use strict";
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
974
1221
|
// src/hooks/lib/hook-failure-signal.ts
|
|
975
1222
|
import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
976
1223
|
import { join as join2, dirname as dirname3 } from "path";
|
|
@@ -1539,9 +1786,6 @@ __export(memory_db_exports, {
|
|
|
1539
1786
|
createSession: () => createSession,
|
|
1540
1787
|
deduplicateFailedAttempt: () => deduplicateFailedAttempt,
|
|
1541
1788
|
dequeuePendingSync: () => dequeuePendingSync,
|
|
1542
|
-
drainRulePromotionEvents: () => drainRulePromotionEvents,
|
|
1543
|
-
drainTeamPromotions: () => drainTeamPromotions,
|
|
1544
|
-
drainTeamRevocations: () => drainTeamRevocations,
|
|
1545
1789
|
embedMissingObservations: () => embedMissingObservations,
|
|
1546
1790
|
endSession: () => endSession,
|
|
1547
1791
|
enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
|
|
@@ -2448,6 +2692,8 @@ function initMemorySchema(db) {
|
|
|
2448
2692
|
`);
|
|
2449
2693
|
migrateObservationEmbeddingChunks(db);
|
|
2450
2694
|
migrateMemoryFilesFor4B(db);
|
|
2695
|
+
ensureRuleCandidatesTable(db);
|
|
2696
|
+
migrateRuleDelivery(db);
|
|
2451
2697
|
}
|
|
2452
2698
|
function enqueueSyncPayload(db, payload) {
|
|
2453
2699
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
@@ -2522,38 +2768,12 @@ function enqueueTeamPromotion(db, promo) {
|
|
|
2522
2768
|
promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
|
|
2523
2769
|
);
|
|
2524
2770
|
}
|
|
2525
|
-
function drainTeamPromotions(db) {
|
|
2526
|
-
const rows = db.prepare(`
|
|
2527
|
-
SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
|
|
2528
|
-
FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
|
|
2529
|
-
`).all();
|
|
2530
|
-
if (rows.length === 0) return [];
|
|
2531
|
-
db.prepare("DELETE FROM team_promotion_outbound").run();
|
|
2532
|
-
return rows.map((r) => ({
|
|
2533
|
-
prompt_hash: r.prompt_hash,
|
|
2534
|
-
destination: r.destination,
|
|
2535
|
-
draft_text: r.draft_text,
|
|
2536
|
-
score: r.score ?? void 0,
|
|
2537
|
-
signals: safeJsonArray(r.signals_json),
|
|
2538
|
-
content_hash: r.content_hash,
|
|
2539
|
-
hardened: r.hardened === 1,
|
|
2540
|
-
review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
|
|
2541
|
-
}));
|
|
2542
|
-
}
|
|
2543
2771
|
function enqueueTeamRevocation(db, promptHash) {
|
|
2544
2772
|
db.prepare(`
|
|
2545
2773
|
INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
|
|
2546
2774
|
VALUES (?, datetime('now'))
|
|
2547
2775
|
`).run(promptHash);
|
|
2548
2776
|
}
|
|
2549
|
-
function drainTeamRevocations(db) {
|
|
2550
|
-
const rows = db.prepare(
|
|
2551
|
-
`SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
|
|
2552
|
-
).all();
|
|
2553
|
-
if (rows.length === 0) return [];
|
|
2554
|
-
db.prepare("DELETE FROM team_revocation_outbound").run();
|
|
2555
|
-
return rows.map((r) => r.prompt_hash);
|
|
2556
|
-
}
|
|
2557
2777
|
function getRecurrenceCountForPromptHash(db, promptHash) {
|
|
2558
2778
|
try {
|
|
2559
2779
|
const row = db.prepare(`
|
|
@@ -2581,30 +2801,14 @@ function enqueueRulePromotionEvent(db, ev) {
|
|
|
2581
2801
|
JSON.stringify(ev.metadata ?? {}),
|
|
2582
2802
|
ev.created_at
|
|
2583
2803
|
);
|
|
2584
|
-
db
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
} catch {
|
|
2804
|
+
capTelemetry(db, FUNNEL_EVENT_OUTBOX_CAP);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
process.stderr.write(
|
|
2807
|
+
`[massu] WARNING: failed to record promotion-funnel event (${ev.event_type} / ${ev.prompt_hash}): ${err instanceof Error ? err.message : String(err)}
|
|
2808
|
+
`
|
|
2809
|
+
);
|
|
2591
2810
|
}
|
|
2592
2811
|
}
|
|
2593
|
-
function drainRulePromotionEvents(db) {
|
|
2594
|
-
const rows = db.prepare(`
|
|
2595
|
-
SELECT id, prompt_hash, event_type, metadata_json, created_at
|
|
2596
|
-
FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
|
|
2597
|
-
`).all(FUNNEL_EVENT_DRAIN_LIMIT);
|
|
2598
|
-
if (rows.length === 0) return [];
|
|
2599
|
-
const maxId = rows[rows.length - 1].id;
|
|
2600
|
-
db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
|
|
2601
|
-
return rows.map((r) => ({
|
|
2602
|
-
prompt_hash: r.prompt_hash,
|
|
2603
|
-
event_type: r.event_type,
|
|
2604
|
-
created_at: r.created_at,
|
|
2605
|
-
metadata: safeJsonParse(r.metadata_json) ?? {}
|
|
2606
|
-
}));
|
|
2607
|
-
}
|
|
2608
2812
|
function recordTelemetry(db, eventType, data) {
|
|
2609
2813
|
try {
|
|
2610
2814
|
db.prepare(`
|
|
@@ -2614,28 +2818,55 @@ function recordTelemetry(db, eventType, data) {
|
|
|
2614
2818
|
} catch {
|
|
2615
2819
|
}
|
|
2616
2820
|
}
|
|
2617
|
-
function
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2821
|
+
function rescueLearningFromStalePayloads(db, stale) {
|
|
2822
|
+
let rescued = 0;
|
|
2823
|
+
for (const item of stale) {
|
|
2824
|
+
let payload;
|
|
2825
|
+
try {
|
|
2826
|
+
payload = JSON.parse(item.payload);
|
|
2827
|
+
} catch {
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
try {
|
|
2831
|
+
const promotions = Array.isArray(payload.rule_promotions) ? payload.rule_promotions : [];
|
|
2832
|
+
for (const p of promotions) {
|
|
2833
|
+
enqueueTeamPromotion(db, p);
|
|
2834
|
+
rescued++;
|
|
2835
|
+
}
|
|
2836
|
+
const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
|
|
2837
|
+
for (const r of revocations) {
|
|
2838
|
+
if (typeof r?.prompt_hash === "string") {
|
|
2839
|
+
enqueueTeamRevocation(db, r.prompt_hash);
|
|
2840
|
+
rescued++;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const events = Array.isArray(payload.rule_promotion_events) ? payload.rule_promotion_events : [];
|
|
2844
|
+
for (const e of events) {
|
|
2845
|
+
enqueueRulePromotionEvent(db, e);
|
|
2846
|
+
}
|
|
2847
|
+
} catch (err) {
|
|
2848
|
+
process.stderr.write(
|
|
2849
|
+
`[massu] WARNING: failed to rescue learned rules from give-up payload ${item.id}: ${err instanceof Error ? err.message : String(err)}
|
|
2850
|
+
`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2630
2853
|
}
|
|
2854
|
+
return rescued;
|
|
2631
2855
|
}
|
|
2632
2856
|
function dequeuePendingSync(db, limit = 10) {
|
|
2633
2857
|
const stale = db.prepare(
|
|
2634
|
-
"SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
|
|
2858
|
+
"SELECT id, retry_count, last_error, payload FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
|
|
2635
2859
|
).all();
|
|
2636
2860
|
if (stale.length > 0) {
|
|
2861
|
+
const rescued = rescueLearningFromStalePayloads(db, stale);
|
|
2637
2862
|
const ids = stale.map((s) => s.id);
|
|
2638
2863
|
db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
|
|
2864
|
+
if (rescued > 0) {
|
|
2865
|
+
process.stderr.write(
|
|
2866
|
+
`[massu] RESCUED ${rescued} learned rule(s) from ${stale.length} give-up payload(s) \u2014 they were re-queued for delivery, NOT discarded.
|
|
2867
|
+
`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2639
2870
|
const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
|
|
2640
2871
|
process.stderr.write(
|
|
2641
2872
|
`[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
|
|
@@ -3327,19 +3558,20 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
|
3327
3558
|
}
|
|
3328
3559
|
return bestMatch;
|
|
3329
3560
|
}
|
|
3330
|
-
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
3561
|
+
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
3331
3562
|
var init_memory_db = __esm({
|
|
3332
3563
|
"src/memory-db.ts"() {
|
|
3333
3564
|
"use strict";
|
|
3334
3565
|
init_config();
|
|
3335
3566
|
init_memory_vector();
|
|
3336
3567
|
init_db_backup();
|
|
3568
|
+
init_rule_delivery();
|
|
3569
|
+
init_rule_candidate_store();
|
|
3337
3570
|
init_hook_failure_signal();
|
|
3338
3571
|
init_memory_embed_sweep();
|
|
3339
3572
|
MEMORY_SCHEMA_VERSION = 1;
|
|
3340
3573
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
3341
3574
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
3342
|
-
FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
|
|
3343
3575
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
3344
3576
|
_temporalColCache = /* @__PURE__ */ new WeakMap();
|
|
3345
3577
|
CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
|
|
@@ -3351,6 +3583,7 @@ var init_memory_db = __esm({
|
|
|
3351
3583
|
|
|
3352
3584
|
// src/hooks/session-end.ts
|
|
3353
3585
|
init_memory_db();
|
|
3586
|
+
init_rule_delivery();
|
|
3354
3587
|
|
|
3355
3588
|
// src/session-archiver.ts
|
|
3356
3589
|
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5, renameSync } from "fs";
|
|
@@ -3631,6 +3864,7 @@ function estimateTokens(text) {
|
|
|
3631
3864
|
// src/cloud-sync.ts
|
|
3632
3865
|
init_config();
|
|
3633
3866
|
init_memory_db();
|
|
3867
|
+
init_rule_delivery();
|
|
3634
3868
|
|
|
3635
3869
|
// src/observation-extractor.ts
|
|
3636
3870
|
init_memory_db();
|
|
@@ -3669,7 +3903,9 @@ function classifyVisibility(title, detail) {
|
|
|
3669
3903
|
// src/cloud-sync.ts
|
|
3670
3904
|
var MAX_RETRIES = 3;
|
|
3671
3905
|
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
3672
|
-
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS =
|
|
3906
|
+
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 8e3;
|
|
3907
|
+
var SYNC_DEADLINE_MS = 2e4;
|
|
3908
|
+
var MIN_ATTEMPT_BUDGET_MS = 1e3;
|
|
3673
3909
|
async function syncToCloud(db, payload) {
|
|
3674
3910
|
const config = getConfig();
|
|
3675
3911
|
const cloud = config.cloud;
|
|
@@ -3752,8 +3988,15 @@ async function syncToCloud(db, payload) {
|
|
|
3752
3988
|
}
|
|
3753
3989
|
}
|
|
3754
3990
|
let lastError = "";
|
|
3755
|
-
const
|
|
3991
|
+
const configuredTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
|
|
3992
|
+
const deadlineAt = Date.now() + SYNC_DEADLINE_MS;
|
|
3756
3993
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
3994
|
+
const remainingMs = deadlineAt - Date.now();
|
|
3995
|
+
if (remainingMs < MIN_ATTEMPT_BUDGET_MS) {
|
|
3996
|
+
lastError = lastError || `sync deadline exceeded (${SYNC_DEADLINE_MS}ms)`;
|
|
3997
|
+
break;
|
|
3998
|
+
}
|
|
3999
|
+
const requestTimeoutMs = Math.min(configuredTimeoutMs, remainingMs);
|
|
3757
4000
|
try {
|
|
3758
4001
|
const response = await fetch(`${endpoint}/sync`, {
|
|
3759
4002
|
method: "POST",
|
|
@@ -3781,6 +4024,10 @@ async function syncToCloud(db, payload) {
|
|
|
3781
4024
|
const result = await response.json();
|
|
3782
4025
|
return {
|
|
3783
4026
|
success: true,
|
|
4027
|
+
// THE ONLY place this is ever set. The server received the payload and
|
|
4028
|
+
// answered 2xx. This — and nothing else — is a receipt, and a receipt is
|
|
4029
|
+
// the sole authority to delete a learned rule.
|
|
4030
|
+
transmitted: true,
|
|
3784
4031
|
synced: {
|
|
3785
4032
|
sessions: result.synced?.sessions ?? 0,
|
|
3786
4033
|
observations: result.synced?.observations ?? 0,
|
|
@@ -3800,8 +4047,12 @@ async function syncToCloud(db, payload) {
|
|
|
3800
4047
|
}
|
|
3801
4048
|
}
|
|
3802
4049
|
try {
|
|
3803
|
-
enqueueSyncPayload(db, JSON.stringify(payload));
|
|
3804
|
-
} catch (
|
|
4050
|
+
enqueueSyncPayload(db, JSON.stringify(stripLearningFromPayload(payload)));
|
|
4051
|
+
} catch (err) {
|
|
4052
|
+
process.stderr.write(
|
|
4053
|
+
`[massu] WARNING: could not queue sync payload for retry: ${err instanceof Error ? err.message : String(err)}
|
|
4054
|
+
`
|
|
4055
|
+
);
|
|
3805
4056
|
}
|
|
3806
4057
|
return {
|
|
3807
4058
|
success: false,
|
|
@@ -4085,6 +4336,9 @@ init_memory_db();
|
|
|
4085
4336
|
// src/rule-candidate-funnel.ts
|
|
4086
4337
|
init_memory_db();
|
|
4087
4338
|
|
|
4339
|
+
// src/rule-candidate-applier.ts
|
|
4340
|
+
init_rule_candidate_store();
|
|
4341
|
+
|
|
4088
4342
|
// src/rule-candidate-hardened.ts
|
|
4089
4343
|
var TEAM_HARDENED_SHAREABLE_DESTINATIONS = [
|
|
4090
4344
|
"pattern-scanner",
|
|
@@ -5690,17 +5944,31 @@ async function main() {
|
|
|
5690
5944
|
}
|
|
5691
5945
|
endSession(db, session_id, "completed");
|
|
5692
5946
|
archiveAndRegenerate(db, session_id);
|
|
5947
|
+
let openLease = null;
|
|
5693
5948
|
try {
|
|
5694
5949
|
await drainSyncQueue(db);
|
|
5695
|
-
const
|
|
5950
|
+
const cloudCfg = getConfig().cloud;
|
|
5951
|
+
const willTransmit = !!cloudCfg?.enabled && !!cloudCfg?.apiKey && !!cloudCfg?.endpoint && cloudCfg?.sync?.memory !== false;
|
|
5952
|
+
const lease = willTransmit ? leaseLearning(db) : null;
|
|
5953
|
+
openLease = lease;
|
|
5954
|
+
const syncPayload = buildSyncPayload(db, session_id, observations, summary, lease);
|
|
5696
5955
|
const result = await syncToCloud(db, syncPayload);
|
|
5697
|
-
if (
|
|
5956
|
+
if (lease) {
|
|
5957
|
+
if (result.transmitted === true) {
|
|
5958
|
+
ackLearning(db, lease);
|
|
5959
|
+
} else {
|
|
5960
|
+
nackLearning(db, lease, result.error ?? "not transmitted (sync disabled or unreachable)");
|
|
5961
|
+
}
|
|
5698
5962
|
}
|
|
5963
|
+
openLease = null;
|
|
5699
5964
|
try {
|
|
5700
5965
|
await pullTeamPromotions(db);
|
|
5701
5966
|
} catch (_pullErr) {
|
|
5702
5967
|
}
|
|
5703
|
-
} catch (
|
|
5968
|
+
} catch (syncErr) {
|
|
5969
|
+
const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
5970
|
+
if (openLease) nackLearning(db, openLease, msg);
|
|
5971
|
+
recordHookFailure("session-end:cloud-sync", syncErr);
|
|
5704
5972
|
}
|
|
5705
5973
|
} finally {
|
|
5706
5974
|
db.close();
|
|
@@ -5710,12 +5978,10 @@ async function main() {
|
|
|
5710
5978
|
}
|
|
5711
5979
|
process.exit(0);
|
|
5712
5980
|
}
|
|
5713
|
-
function buildSyncPayload(db, sessionId, observations, summary) {
|
|
5714
|
-
const
|
|
5715
|
-
const
|
|
5716
|
-
const
|
|
5717
|
-
const revocations = willTransmit ? drainTeamRevocations(db) : [];
|
|
5718
|
-
const funnelEvents = willTransmit ? drainRulePromotionEvents(db) : [];
|
|
5981
|
+
function buildSyncPayload(db, sessionId, observations, summary, lease) {
|
|
5982
|
+
const promotions = lease?.promotions ?? [];
|
|
5983
|
+
const revocations = lease?.revocations ?? [];
|
|
5984
|
+
const funnelEvents = lease?.events ?? [];
|
|
5719
5985
|
return {
|
|
5720
5986
|
sessions: [{
|
|
5721
5987
|
local_session_id: sessionId,
|