You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Six medium findings from the audit. Each is pinned by a regression test.
sync/records.js: pruneRecords' only "don't prune after a failure" gate was
RUN-wide, so one converged row in any other table (counted as skipped)
disarmed it. A FIELD_FORBIDDEN 403 is a per-field permission property that
fails every row of exactly one table by design, so a wholly-failed table is
routine -- and under mirror+confirmDeletions its pre-existing dest rows were
deleted while the replacement data was never written, reporting phase=done.
Pass 1 now records per-table outcomes and prune skips a table that wrote
nothing and failed, emitting RECORDS_FAILED_PRUNE_SKIPPED.
sync/prune-schema.js: pruneSchema deleted the fieldMappings targets apply()
had just validated. A target must exist on the dest but not on the source --
the documented injection pattern (source autoNumber Code -> dest text field
InjectID) is dest-only by construction and so always an orphan. The records
job then re-validated, threw FIELD_MAP_INVALID and synced zero records: a
destination column destroyed and no data written.
sync/index.js: the drift guard fingerprints only the DESTINATION, but
plan.orphans is a statement about the SOURCE. A source-side addition between
plan and apply was invisible -- dest untouched so the fingerprint matched, no
DRIFT, no create action in the stale plan -- and pruneSchema then deleted the
dest field that now legitimately matched. expectedName cannot help; the field
kept its name. apply() now re-reads the source before any schema deletion and
drops orphans that regained a counterpart; if that read fails it skips all
schema deletion rather than trusting a stale list.
daemon/launcher.js: stopDaemon --force SIGKILLed a lockfile pid with no
identity check. isStale()'s bare process.kill(pid,0) cannot tell our daemon
from a process that recycled the pid, and on Windows SIGTERM maps to
TerminateProcess -- an unsavable hard kill of a stranger. Now probes
/daemon/health and requires the uuid to echo the lockfile's, mirroring the
extension's _verifyDaemonIdentity. Unproven means release the lock and leave
the process alone.
sync/records.js: buildUpdateCells never compared against the destination, so
every mapped scalar cell of every mapped row was re-posted on each re-sync --
updateRecords issues one serialized POST per CELL, i.e. 20,000 sequential
requests for a 1000-row x 20-field no-op run. Converged cells are now skipped.
language-services diagnostics.ts: isInsideExclusionRange was a linear scan run
once per character by three checkers, so cost was chars x field-refs with no
debounce and no size cap -- and in --tcp daemon mode that blocking is shared
by every attached editor. Now a binary search over the already-ascending
ranges. Measured on the repo's largest shipped example (38,830 chars / 741
refs): 83.3ms -> 9.6ms. On an 87KB/4000-ref synthetic: 592ms -> 10ms.
Diagnostic output identical.
Verified: 1451 mcp-server + 162 language-services + 88 webview tests pass,
check:tool-sync green, pnpm build succeeds. One extension test
(session-backup "rejects backups larger than 200 MB") fails with ENOSPC --
the disk is 100% full and that test truncates a 201 MB file. It passed
earlier this session and neither it nor packages/extension is touched by
this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reason: `Refusing to force-kill pid ${pid}: it did not answer /daemon/health with this lockfile's uuid, so it cannot be shown to be our daemon — the pid may have been recycled by an unrelated process. The stale lock was released, so a new daemon can start normally. If you are certain pid ${pid} is a hung airtable-user-mcp daemon, end it yourself.`,
message: `${o.kind} "${o.name}"${o.tableName ? ` in "${o.tableName}"` : ''} was planned for deletion but now has a source counterpart — the source changed after plan ${planId}. Kept. Re-run mode=plan to sync it.`,
// Cannot prove the orphan list is current → do not delete on a stale one.
373
+
prunePlan={ ...fullPlan,orphans: []};
374
+
result.warnings.push({
375
+
code: 'ORPHAN_RECHECK_FAILED',
376
+
message: `Could not re-read the source to confirm ${orphanList.length} planned deletion(s) are still orphans (${e.message??e}); skipped all schema deletions this run.`,
377
+
});
378
+
}
379
+
}
380
+
381
+
// fieldMappings is threaded through so pruneSchema never deletes a mapping
382
+
// TARGET — those are dest-only by construction and therefore always orphans.
0 commit comments