improve gmail sync: email input, permanent errors, auto-dock

- add isync to setup.sh
- ask for gmail address alongside app password in integrations
- add PermanentError to job queue (skips retries for non-recoverable failures)
- use PermanentError for missing credentials, missing executable, auth failures
- auto-add /email to dock after successful gmail sync
- invalidate dock cache on sync completion for seamless UI update

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 23:13:55 +00:00
co-authored by Claude Opus 4.6
parent 8737681464
commit aa0207436c
7 changed files with 146 additions and 43 deletions
+13 -6
View File
@@ -1,4 +1,4 @@
import type { Job, JobProgress, EnqueueParams, StepContext } from './types';
import { type Job, type JobProgress, type EnqueueParams, type StepContext, PermanentError } from './types';
import { readJob, writeJob, listAllJobs } from './storage';
import { getHandler } from './handler-registry';
import { sendMail } from 'emailer';
@@ -103,7 +103,9 @@ async function processNextInLane(lane: string) {
console.error(`[queue] Lane ${lane} processing error:`, err);
} finally {
const jobs = await listAllJobs();
const hasMore = jobs.some((j) => j.lane === lane && j.status === 'queued' && (!j.retryAt || j.retryAt <= Date.now()));
const hasMore = jobs.some(
(j) => j.lane === lane && j.status === 'queued' && (!j.retryAt || j.retryAt <= Date.now()),
);
if (hasMore) {
processNextInLane(lane);
} else {
@@ -127,7 +129,9 @@ async function runJob(job: Job) {
job.retryAt = undefined;
await writeJob(job);
const isRetry = (job.retries ?? 0) > 0;
console.log(`[queue] ${isRetry ? 'Resuming' : 'Running'} job ${job.id} (${job.type})${isRetry ? ` retry ${job.retries}` : ''}`);
console.log(
`[queue] ${isRetry ? 'Resuming' : 'Running'} job ${job.id} (${job.type})${isRetry ? ` retry ${job.retries}` : ''}`,
);
const sharedMeta: Record<string, unknown> = { ...(job.meta ?? {}) };
@@ -182,9 +186,10 @@ async function runJob(job: Job) {
step.error = errorMessage;
step.completedAt = Date.now();
// Check if handler supports retry
// Check if handler supports retry (PermanentError skips retries)
const isPermanent = err instanceof PermanentError;
const retries = (fresh.retries ?? 0) + 1;
if (handler.retry && retries <= handler.retry.maxRetries) {
if (!isPermanent && handler.retry && retries <= handler.retry.maxRetries) {
// Schedule retry: reset failed step to pending, re-queue
step.status = 'pending';
step.error = undefined;
@@ -198,7 +203,9 @@ async function runJob(job: Job) {
fresh.retries = retries;
fresh.retryAt = Date.now() + handler.retry.delayMs;
await writeJob(fresh);
console.log(`[queue] Job ${fresh.id} will retry (${retries}/${handler.retry.maxRetries}) in ${handler.retry.delayMs / 1000}s — failed at step "${step.name}": ${errorMessage}`);
console.log(
`[queue] Job ${fresh.id} will retry (${retries}/${handler.retry.maxRetries}) in ${handler.retry.delayMs / 1000}s — failed at step "${step.name}": ${errorMessage}`,
);
scheduleRetry(fresh.lane, handler.retry.delayMs);
return;
}