Don't break the entire run when LinkedIn rate limits — skip LinkedIn
jobs and continue to Wellfound and other platforms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When LinkedIn shows "Easy Apply limit" message, detect it in the
modal-open check, return rate_limited status, put job back in queue,
send Telegram alert, and stop the entire run. Prevents burning retries
and wasting browser sessions when rate limited.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Profile name is returned by ensureAuth() from the auth connection
(looked up by domain). No more storing profile names in settings.json
for the applier flow. createBrowser() still supports legacy platform
keys as fallback for searcher/setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connection IDs change when re-authing. Instead of storing IDs in
settings.json (which go stale), look up connections by domain at
runtime via kernel.auth.connections.list({ domain }). This keeps
the applier in sync regardless of connection recreation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite session.mjs: check connection status via SDK before creating
browser. If NEEDS_AUTH + can_reauth, auto re-auth with stored creds.
If can't re-auth, send Telegram alert and skip platform.
- Wire ensureAuth() into job_applier.mjs before createBrowser()
- Jobs are returned to queue (not failed) when auth is down
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stuck/incomplete retry logic referenced maxRetries which was only
defined in main() scope, not in handleResult(). Compute it locally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stuck and incomplete jobs now get retried up to max_retries (default 2)
before being permanently marked. Honeypots are still permanent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each job now shows title @ company with a clickable link, grouped by
status category. Only non-empty categories are shown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Distinguishes closed listings from missing apply buttons. Shows in summary
as a separate line item.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
logStream.end() callback wasn't firing reliably, leaving processes hanging.
process.exit() is synchronous and forces exit regardless of open handles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All entry points with log tee now call logStream.end() + process.exit()
(log stream kept event loop alive, blocking next cron run)
- easy_apply: detect "no longer accepting applications" and similar closed
listing text before reporting as unsupported
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused APPLY_PRIORITY array (replaced by score-based sort)
- Fix run timeout only breaking inner loop — now breaks outer platform loop too
- Remove dead lastProgress variable in easy_apply step loop
- Add stdout/stderr log tee to job_searcher, job_filter, telegram_poller
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Writes all console output to a log file from inside the process so
logs are always available regardless of how the process is launched.
Fixes invisible output when claw redirects stdout to a file that
gets overwritten by a second lock-blocked attempt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Default case in handleResult now increments skipped_other
- Summary only shows non-zero categories (cleaner output)
- Applied count always shown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every unknown required field now goes through AI before falling back to
Telegram. Claude sees the question + all saved answers + profile, and
either recognizes it as a variation of a saved answer or generates a new
one. AI answers are auto-saved to answers.json so the same question is
a free pattern match next time. Telegram is now last resort (no API key).
Flow: pattern match (free) → AI (smart) → auto-save → Telegram (human)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log lines now explicitly say STOPPING with reason when the modal is
dismissed due to unknown questions or honeypots. handleResult logs
the full flow: paused → generating AI answer → sent to Telegram →
will retry after reply. Prevents claw from misreading as a hang/timeout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New lib/telegram_answers.mjs: shared module that polls Telegram getUpdates,
matches replies to needs_answer jobs, saves to answers.json, flips job to new
- telegram_poller.mjs: lightweight cron script (every minute via OpenClaw)
- Applier also processes replies at start of each run as safety net
- sendTelegram now returns message_id, stored on job for reply matching
- User replies "ACCEPT" to use AI answer, or types their own
- Answers persist in answers.json and apply to ALL future jobs
- Also includes: selectOptionFuzzy, multiple dialog handling, browser
recovery, answers reload, per-job timeout bump to 10min
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Easy Apply click: click found element directly, retry with force if modal doesn't open
- Resume: select first radio if none checked, fall back to file upload
- AI answers: inject stored answers into formFiller on needs_answer retry
- Answers persistence: reload answers.json before each job for Telegram replies
- Browser recovery: detect dead page, create fresh browser session
- Multiple dialogs: findApplyModal() tags the right dialog among cookie banners etc.
- Select matching: case-insensitive fuzzy match with substring fallback
- dismissModal: scope Discard scan to dialog elements only
- Label dedup: normalize whitespace, fix odd-length edge case
- no_modal status: explicit handleResult case
- Per-job timeout: 10 minutes (was 3)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a required select has no answer, the unknown field now includes
the available options. Telegram notification shows them so the user
knows exactly what to reply with.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split skipped_no_easy_apply into skipped_no_apply (no button/modal)
and skipped_other (honeypot/stuck/incomplete) for clearer reporting
- Update Telegram summary, status.mjs display, and job_applier counters
- Add missing skipped_easy_apply_unsupported to README status table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- lib/apply/index.mjs: add STATUS_MAP to normalize platform-specific statuses
to generic ones (no_button/no_submit/no_modal → skipped_no_apply).
Documented all generic statuses for AI/developer reference.
- job_applier.mjs: handleResult now handles skipped_no_apply, default case
logs + saves instead of silently dropping
- lib/linkedin.mjs: remove dead applyLinkedIn() and detectAts(), clean imports
(~110 lines removed). Search-only module now.
- lib/wellfound.mjs: remove dead applyWellfound(), clean imports.
Search-only module now.
- lib/lock.mjs: fix async signal handler — shutdown handlers now actually
complete before process.exit()
- test_linkedin_login.mjs: add try/catch/finally with proper browser cleanup
- README: update status table with all current statuses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- job_applier.mjs: stack traces on per-job and browser-level errors
- session.mjs: log pending status and poll count during session refresh
- linkedin.mjs: log warning when job card click fails instead of silent catch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused readFileSync import from job_applier.mjs
- Remove unused makeJobId (dead code, nothing imports it)
- setup.mjs: use shared loadConfig instead of inline cfg()
- queue.mjs: add in-memory cache for queue and log to avoid
redundant disk reads during a single run
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add loadConfig() helper with clear errors for missing/malformed JSON
- Replace raw JSON.parse(readFileSync(...)) in both entry points
- Track retry_count on jobs; re-queue as 'new' up to max_retries (default 2)
- Add max_retries and DEFAULT_MAX_RETRIES constant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove random suffix from Wellfound job IDs (broke dedup)
- Add null coalescing to all profile field returns in form_filler
- Fix honeypot case referencing nonexistent results.skipped counter
- Remove unused makeJobId import from linkedin.mjs
- Navigate directly to job URL instead of search+click in linkedin apply
- Add Telegram notification rate limiting (1.5s between sends)
- Replace Mode B blocking sleep with --preview flag
- Add max_applications_per_run enforcement
- Remove tracked search_config.json (keep .example.json only)
- Add search_config.json to .gitignore, fix duplicate node_modules entry
- Extract all magic numbers/strings to lib/constants.mjs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>