fix: auto-version-bump swallowed operator-direction; ptrmap + lock guards

- sf-db-schema.js: auto_vacuum INCREMENTAL → NONE. The "Bad ptr map entry"
  corruption on 2026-05-17 was incremental-autovacuum ptrmap drift under
  concurrent writers. Recovered DB has no ptrmap; future fresh DBs must
  match. incremental_vacuum() callers in sf-db-core.js become no-ops.
- bin/sf-from-source: lock allowlist extended to skip readonly sf headless
  subcommands (--help, query, status, usage, reflect, feedback list,
  triage --list/--json). Previously every sf headless invocation tried
  to acquire the project lock — operator couldn't even inspect SF state
  while autonomous was running.
- self-feedback.js triageBlockedEntries: (1) treat empty/null/undefined
  sfVersion as unknown, not zero; (2) exempt operator-direction kinds
  (improvement-idea, architecture-defect, missing-feature, gap) from
  auto-version-bump close. Both were needed to prevent the R124 incident
  recurring.
- headless-feedback.ts handleAdd: populate sfVersion via getCurrentSfVersion
  + detect repoIdentity via isForgeRepo, not hardcoded "external"/"". An
  empty sfVersion sorts below any real semver, so the resolver retry-closed
  every operator-filed entry within seconds.

Net effect: R124 proposal (filed via sf headless feedback add) is no
longer auto-resolved as version-stale. Larger architectural fix (single-
writer SF daemon / RPC for all DB writes — M040 territory) tracked as
follow-up R-entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-17 15:51:36 +02:00
parent 87e9729c13
commit 1cd7890d64
2 changed files with 62 additions and 3 deletions

View file

@ -151,14 +151,35 @@ async function handleAdd(
const id = newId();
const ts = new Date().toISOString();
// Detect forge-vs-external repo via the canonical helper used by
// recordSelfFeedback; this affects which channel the entry is read from.
// Previously hardcoded "external", which incorrectly tagged forge-repo
// entries — combined with sfVersion="" below, this fed the
// auto-version-bump resolver false positives that swallowed legitimate
// operator-direction within seconds of filing (R124 incident 2026-05-17).
const sfHelpers = (await jiti.import(sfExtensionPath("self-feedback"), {})) as {
isForgeRepo?: (basePath: string) => boolean;
getCurrentSfVersion?: () => string;
};
const repoIdentity: "forge" | "external" = sfHelpers.isForgeRepo?.(basePath)
? "forge"
: "external";
// sfVersion must reflect the SF version that filed the entry. An empty
// string sorts BELOW any real semver — so triageBlockedEntries would
// retry-close every empty-version entry as "version-bumped" (R124
// incident 2026-05-17). Use the canonical helper + env fallback.
const sfVersion =
sfHelpers.getCurrentSfVersion?.() ??
process.env.SF_VERSION ??
"unknown";
const entry = {
id,
ts,
kind,
severity,
blocking,
repoIdentity: "external" as const,
sfVersion: "",
repoIdentity,
sfVersion,
basePath,
occurredIn: {
unitType: unitType ?? null,

View file

@ -970,12 +970,50 @@ function compareSemver(a, b) {
}
return 0;
}
/**
* Self-feedback kinds that represent operator intent rather than mechanical
* symptoms. Auto-version-bump close MUST NOT fire on these operator-filed
* design / capability / spec direction does NOT expire when SF's version
* bumps. Mechanical symptoms (runaway-loop:idle-halt, prompt-quality-issue,
* silent-worker-failure) ARE retry-eligible because the underlying bug may
* have been fixed in the bumped version.
*
* Observed 2026-05-17: operator filed `improvement-idea` (R124 proposal) via
* `sf headless feedback add`; entry written with empty sfVersion; resolver
* compareSemver'd "" against "2.75.4" close as "version-bumped" within 54
* seconds. Two defects, both fixed here:
* 1) empty/null/undefined sfVersion treated as comparable (now treated as
* unknown).
* 2) operator-direction kinds NOT exempt from the close path (now skipped).
*/
const OPERATOR_DIRECTION_KINDS = new Set([
"improvement-idea",
"architecture-defect",
"missing-feature",
"gap",
]);
function isMissingVersion(v) {
return (
v === undefined ||
v === null ||
v === "" ||
v === "unknown" ||
v === "null" ||
v === "undefined"
);
}
export function triageBlockedEntries(basePath = process.cwd()) {
const current = getCurrentSfVersion();
const retry = [];
const stillBlocked = [];
for (const e of getBlockedEntries(basePath)) {
if (current === "unknown" || e.sfVersion === "unknown") {
if (OPERATOR_DIRECTION_KINDS.has(e.kind)) {
stillBlocked.push(e);
continue;
}
if (isMissingVersion(current) || isMissingVersion(e.sfVersion)) {
stillBlocked.push(e);
continue;
}