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:
parent
87e9729c13
commit
1cd7890d64
2 changed files with 62 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue