# Changelog of SAM.gov Scraper – New Contract Opportunity Alerts (`cce_partners/samgov-opportunity-radar`) Actor

- **URL**: https://apify.com/cce_partners/samgov-opportunity-radar/changelog.md
- **Full Actor documentation**: https://apify.com/cce_partners/samgov-opportunity-radar.md

## Changelog

### 0.2.22 (Oct 16, 2026)

- No changes to how the Actor runs: output, charges, saved state and status messages are identical to 0.2.21.
- Maintenance: the source now includes the maintainer's tool for cleaning up the saved state of test monitors (`tools/cleanup_keys.py`). It is not part of the Actor image and never runs during a run.
- No pricing changes.

### 0.2.21 (Oct 16, 2026)

- **Fix: a monitor run that lost its lock can no longer wipe out the new run's pending copy.** Each batch's pending copy (used to re-send it free of charge after an interrupted run) was kept in one shared record per monitor. A write that was still on its way from a run that had lost the monitor could land on that shared record after another run had taken over: either deleting the new run's pending copy, or replacing it with an older one that was then ignored. If the new run was also killed before pushing that batch, those notices (up to one batch) were never delivered, and the log said "Ignored a pending batch ... delivered normally" although they were not. Each run now keeps its own pending copy (`PENDING-STATE_<name>!<run ID>`), so a late write from one run can only touch its own copy. The next run on the monitor reads all of them and re-sends what is still pending, free of charge. Nothing is lost and nothing is charged twice.
- A pending copy left by an older version (`PENDING-STATE_<name>`) is still picked up and re-sent free, so you can update while an interrupted batch is waiting. Pending copies are deleted once handled, so they don't pile up; a run never deletes a copy another run has changed in the meantime.
- If you clean up saved state by hand: a monitor can now have several `PENDING-STATE_<name>!...` records after interrupted runs; delete them together with the monitor's other records (README, "What is saved").
- Cost: unchanged for normal runs (same reads, writes, output, charges and status messages as 0.2.19; one key listing per monitor run). A run that re-sends an interrupted batch makes two more small reads.
- No pricing changes. No changes for runs without a monitor name, and still no saved-state access at all for an empty input or the Store's example input.

### 0.2.19 (Oct 16, 2026)

- **A run whose own lock went 9 minutes without a heartbeat no longer pushes or charges.** 0.2.18 stopped such a run from saving (and from pushing once another run had taken the monitor over); with only a stale heartbeat it could still push the notices of the batch it had just saved and charge for them, while another run may already own the monitor. The checks right before the push, before clearing the pending copy and before the charge now use the same 9-minute rule. The run stops with the status "Stopped early: this run's monitor lock expired, so it stopped saving, pushing and charging." (or "Stopped early: another run took over this monitor, so this run stopped saving, pushing and charging.") and keeps the pending copy, so the next run re-sends that batch free of charge. Nothing is charged twice.
- **Fix: a late save from a run that lost its monitor can no longer undo the new owner's progress.** A write that was already on its way when the run stalled (for example a hung request that lands minutes later) could overwrite the state the new owner had just saved. 0.2.18 did not double-charge in that case, but a later real change to a notice could be silently missed, and a late pending copy could make the next run re-send notices for free a second time. Every save is now stamped with the run that wrote it and when that run took the monitor, and is written twice (the saved state and a second copy named FP2B-STATE\_...). The next run uses whichever copy is newer, ignores an older save that landed late, and only re-sends pending notices that nobody saved since. No notices are lost and nothing is charged twice.
- Existing saved states (0.2.18 and older) load unchanged on the first run and are upgraded on the next save. Rolling back to 0.2.18 or 0.2.17 also works: they read the new format and ignore the second copy.
- New key: each monitor (and each saved state of a run without a monitor name that still uses one, for example FP2B-STATE_46f35bdfc6d3) gets one more small record, FP2B-STATE\_..., the same size as the existing FP2-STATE\_ record. It is an expected key from now on: if you clean up saved state, delete it together with the monitor's other records (README, "What is saved").
- Cost: each time the state is saved there is one more small write (the second copy) and one more lock check before it; a run that continues a saved monitor also reads the second copy once (measured on a 40-notice first run: 38 instead of 32 key-value reads, 23 instead of 18 writes). A tiny fraction of a cent per run. Output, charges and status messages are identical to 0.2.18 for normal runs.
- **Runs with no filters and the Store's example input never touch the saved-state store.** An empty input, an empty monitor name with no filters, and the example input (NAICS 236220) were already stateless, but they still checked whether an older version had saved state for them (to suggest a monitor name in the log). They now make no reads or writes there at all, and the example input is treated like an empty input: older no-name state for exactly NAICS 236220 is no longer picked up automatically; set monitorName "c2a2d3548358" to keep using it ("07f8910e3b79" for runs with no filters). Every other filter set without a monitor name keeps its older saved state as before.
- **If you used this Actor before version 0.2.8 without a monitor name and with only the NAICS code 236220 as a filter** (the Store's example input), your old saved state is no longer used automatically: those runs now return the newest notices like any run without a monitor name. To keep getting only new or changed notices from that saved state, set monitorName to "c2a2d3548358". Nothing is re-sent or charged again when you do. Runs with other filters are not affected.
- No pricing changes.

### 0.2.18 (Oct 16, 2026)

- **Fix: a monitor run that loses its lock in the middle of saving stops saving at once.** Saving a batch writes several records one after the other (the pending copy, then the saved state, then a copy for older versions). The run checked that it still owned the monitor only before the first of them. If another run took the monitor over in between (after the first run stalled for 10 minutes without a heartbeat, or right after it released the lock just before its run timeout), the first run could still write the rest and overwrite what the new run had saved. In rare cases that could charge notices a second time on a later run, or leave a batch marked as delivered that was never delivered. The run now checks the lock again right before each record it writes, and stops as soon as it is no longer the owner. A batch cut off this way is re-sent free of charge by the next run (or delivered by the run that took over); nothing is charged twice.
- **A run whose own lock went 9 minutes without a heartbeat stops saving** (for example when the state store was unreachable for that long), because another run may take the monitor over at any moment. It ends with the status message "Stopped early: this run's monitor lock expired ..."; the next run continues and re-sends any interrupted batch free of charge.
- Cost: a few more reads of the small lock record per run (2 per batch of 100 notices plus 1 per run; one more each for monitor names with leading or trailing spaces), a tiny fraction of a cent per run. Nothing else changes for normal runs; output, charges and saved state are identical to 0.2.17.
- No pricing changes. No changes for runs without a monitor name.

### 0.2.17 (Oct 16, 2026)

- **Fix: a slow save near the run timeout no longer blocks the monitor for 10 minutes.** When a run stopped just before its timeout while its saved progress was still being written, it could keep the monitor locked until the platform ended the run; the next runs then stopped as "Locked" for up to 10 minutes. The run now releases the lock in time. Nothing is lost: a batch cut off there is re-sent free of charge by the next run.
- **Fix: runs without a monitor name always return the full number of results.** In keyless mode, a notice listed twice in the nightly extract used to be counted twice toward `maxItems` and then removed, so a run could return fewer rows than `maxItems`. Duplicates are now removed first, so you get `maxItems` different notices (when that many match).
- **The same copy every time.** When SAM.gov lists the same notice twice with the same posted date but different details, every run now keeps the same copy, instead of whichever came first. Notices posted at the same time are also always listed in the same order.
- **Fix: very large batches can no longer make monitor runs fail over and over.** The safety copy of each batch (used to re-send it for free after an interrupted run) must fit in one 8 MB record. If fields other than the description made it too big, every run on that monitor failed. The copy is now shortened further when needed, and every notice stays in it. This can't happen with normal SAM.gov data.
- README: what to expect from re-sent rows after an interrupted run. They can have shortened descriptions (only for very large batches), they're re-sent whenever the next run on that monitor happens (the safety copy doesn't expire), and they can fall outside filters you changed since.
- README: the other Actors in the GovCon suite are listed by their current names, and the new Federal Contract Awards – New Winners Feed is added.
- No pricing changes.

### 0.2.16 (Oct 15, 2026)

- **Interrupted batches are re-sent for free.** Before a batch of results is saved as delivered, a monitor run now keeps a copy of that batch as "pending", and clears it once the batch is pushed. If a run is killed in between (a forced abort, an out-of-memory crash, a run timeout), the next run on the same monitor first pushes that batch again **without charging it**, then continues normally. Before, such a batch was marked as delivered but never reached your dataset. A kill right after the push can make the free re-send a duplicate of rows you already have; a notice is never charged twice.
- **Fix: a graceful stop no longer loses a slow batch.** On a graceful abort, a stop signal or the run-timeout guard, the run used to wait at most 5 seconds for the batch in progress. A slower batch could end up saved as delivered but never pushed. The run now waits for the batch to finish its push and charge, up to just before the run timeout (a graceful abort gets 30 seconds from Apify). A batch cut off at that point is re-sent free by the next run.
- **Fix: no duplicate rows in runs without a monitor name.** With `maxItems` above 1,000 in API-key mode, overlapping result pages could return (and charge) the same notice twice, and so could a notice listed twice in the nightly extract. Every run now returns each notice ID once.
- The log no longer says "Lost the monitor lock" when the run released the lock itself while stopping.
- README: the default Abort button in Apify Console is a forced, immediate abort. It can block a monitor for up to 10 minutes and gives the run no time to finish its batch; a graceful abort releases the lock. Runs without a monitor name that still use saved state from 0.2.7 or earlier also take the lock. A run that stops as "Locked" still pays the Actor start charge.
- No pricing changes. No changes for runs without a monitor name, except the duplicate fix.

### 0.2.15 (Oct 15, 2026)

- **One run per monitor at a time (run lock).** A run with a monitor name (or a grandfathered no-name saved state) locks that monitor while it works. A second run on the same monitor stops right away with a "Locked" status message and returns and charges no notices, so nothing is delivered or charged twice. A lock whose run is still running, aborting or timing out is respected; a lock whose run has ended is taken over. If the Actor can't read the other run's status (default limited permissions), the lock expires 10 minutes after its last heartbeat. Runs without a monitor name never take a lock and still write nothing.
- **Clean stops.** The lock is released (and waited for) on every way a run ends: normal end, errors, a platform abort, a stop signal (SIGTERM, or SIGINT / Ctrl+C locally, exit code 130) and the run-timeout guard. A run whose lock was taken over stops pushing and charging at once, writes no more saved state and never touches the other run's lock.
- **Run-timeout guard.** A platform timeout gives a run no time to clean up, so a monitor run now stops itself 15 seconds before its timeout (a third of the time left for very short timeouts), releases the lock and ends as failed with the message "Stopped N s before the run timeout so the monitor lock is released ...". Notices already returned are saved and won't be charged again; the next run continues.
- **Fix: an abort no longer lands between a batch's save and its push.** Before, a stop right after a batch was saved as delivered could end the run before that batch was pushed: those notices were never charged, but also never delivered. Now the batch in progress finishes its push and charge (up to 5 s) before the run releases the lock and exits.
- Known limits: a forced abort or an out-of-memory crash gives the run no time to release its lock, so the monitor can be blocked for up to 10 minutes (runs in that time stop as "Locked" and charge no notices). Such a crash exactly between a batch's save and its push, or a monitor taken over by another run at that moment, can still leave that batch undelivered (never charged).
- No pricing changes. No changes for runs without a monitor name.

### 0.2.11 (Oct 15, 2026)

- **Fix: switching from API-key mode to keyless mode no longer re-charges a recent amendment.** The keyless nightly extract is up to ~24 h behind the API. A notice amended shortly before the switch was charged once in API-key mode and up to twice more in keyless mode (once for the stale extract copy, once when the extract caught up). Now a monitor remembers, for 7 days, what API-key mode returned and what it replaced; keyless copies of that content are not returned or charged. One amendment is charged once across mode switches (one documented exception in the README).
- **Fix: monitor names with leading or trailing spaces keep their 0.2.7 history.** 0.2.7 used the name untrimmed for its saved state (`" rev "` -> `STATE_-rev-`), later builds trim it (`STATE_rev`), so upgrading from 0.2.7 re-charged the whole monitor once. The old key is now read as well (migration) and kept in sync, so rolling back to 0.2.7 stays consistent too.
- **Existing no-name users keep "only new" tracking (grandfathered).** A run without a monitor name whose account already has the pre-0.2.8 saved default state for the same filters (the implicit filter-hash state, `STATE_<hash>` or `FP2-STATE_<hash>`) keeps using it: only new or changed notices are returned and charged, default `maxItems` 1,000 as in 0.2.7, and the log says "Stateless (latest rows) is now the default for runs without a monitor name; your existing saved state is kept. Set monitorName "<hash>" to keep it explicitly." The status message carries a short form ("Legacy no-name state kept; set monitorName "<hash>" to keep it."). That suggested `monitorName` (the 12-character filter hash) maps to the same saved record, so switching to it re-sends and re-charges nothing. Runs with no such state stay stateless (newest 25, no records written; on a brand-new account an empty storage may be created).
- **Runs with no filters (an empty input or the Store's example input) are never grandfathered.** They are always stateless (newest 25), so they always return data, even with saved state from 0.2.7 or earlier for that filter set. If such state exists, it is only read, never written, and the log names the monitorName that keeps it ("Saved state from an older version exists for these filters, but runs without a monitor name and without filters are always stateless (latest rows). Set monitorName "07f8910e3b79" to get only new or changed notices from that saved state."); the status message has a short form ("Saved state found; set monitorName "07f8910e3b79" for only new results."). Every other filter set is grandfathered as described above.
- Monitor names with leading or trailing spaces now always keep the untrimmed 0.2.7 key in sync (also for monitors started on 0.2.8 or later), so rolling back to 0.2.7 does not re-charge them.
- Stateless run log reports the number of notices scanned (not "matched").
- Status messages are kept at 500 characters or fewer (Apify cuts longer ones); if needed, whole lower-priority notes are dropped.
- **State cap prunes closed notices first.** Above 50,000 saved notices a monitor now forgets closed notices (deadline passed), then notices without a deadline, and open notices only as a last resort, with a warning in the log.
- Wording: README, the `maxItems` input description and the pricing copy now say clearly that runs without a monitor name return the newest notices (25 by default) with no change tracking, and every returned notice is charged.
- The independent review's tests are part of `npm test`. No pricing changes.

### 0.2.10 (Oct 15, 2026)

- **Fix: no re-charges when nothing really changed.** A monitor's "changed" check now uses a versioned fingerprint (v2) of the notice content only: title, solicitation number, notice type, posted date (date only), response deadline (date only), set-aside, primary NAICS, PSC, award number, award date and award amount, normalized so keyless and API-key mode give the same result. Adding or removing an API key, archiving, agency/office renames and link format changes no longer re-deliver and re-charge notices.
- **Silent upgrade.** The first run of an existing monitor on 0.2.10 records the new fingerprints without outputting or charging anything (status: "Fingerprint v2: N known notices re-baselined"). The previous saved state is kept up to date in its old format, so going back to an older build does not re-charge either.
- **Nothing is skipped by maxItems anymore.** With a monitor name, every matching notice is compared first, then `maxItems` caps the output; new or changed notices beyond it come on the next run.
- **Charge limit respected.** Results are pushed and charged in batches; when your maximum charge per run is reached the run stops cleanly and the rest comes on the next run. Saved state is written before each batch is charged, so an aborted run never charges a notice twice.
- API-key mode results are sorted newest first explicitly. Saved state is capped (notices that can no longer match are removed).
- No pricing changes.
- **Known limits** (also in the README, "How change detection works"): a deadline moved to another time on the same date, or a title edit that only changes capitalization or punctuation, is not reported as changed; a real amendment published exactly between a monitor's last run on an older build and its first run on 0.2.10 (or during a rollback and return) is absorbed once without being reported; if a run crashes after a batch was saved but before it was pushed, that batch is never charged but may never be delivered; a closed notice the monitor forgot during state cleanup (deadline passed 90+ days, unseen 90+ days, or posted over a year ago) is returned and charged once as new if it shows up again.

### 0.2.9 (Oct 15, 2026)

- **Fix:** notices output only because of "Also output unchanged" (`emitUnchanged`, `changeType: "unchanged"`) are no longer charged, as the input description and README always said. Before, they were charged like new rows (per-row `opportunity` event, and `ai-tag` when tagging is on). New and changed notices are charged exactly as before; prices are unchanged.
- Monitor status message now shows unchanged rows separately, e.g. "0 new or changed notices + 30 unchanged (not charged) (30 matched)."

### 0.2.8 (Oct 15, 2026)

- **Stateless by default.** Saved "only new or changed" state is used only when you set a monitor name (`monitorName`). Without one, each run returns the newest notices posted in the last `lookbackDays` days and reads or writes no saved state; `changeType` is empty on those rows.
- Monitors are no longer created implicitly from a hash of your filters, and that old filter-derived state is not used anymore (no carry-over). Scheduled runs and integrations that want only new notices must set a monitor name; their first run returns the whole window.
- `maxItems` has no schema default now: 25 for runs without a monitor name, 1,000 with one. A value you set always wins.
- Runs without a monitor name end with the status message "Set a monitor name to get only new results."; monitor runs report new/changed counts.
- No pricing changes: $0.01 per run + $0.004 per notice returned (+ $0.002 per AI tag when enabled), charged per row pushed.
- Input descriptions and README updated ("Monitor names: get only new results").
