| @@ -2871,8 +2871,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2870 unmodified lines |
| 2871 | 2871 | // column only if this exact value is still in it, so a mark written |
| 2872 | 2872 | // while we were dialing carries a different token, survives the clear, |
| 2873 | 2873 | // and re-lists the connection on the next read. |
| 2874 | | − const observedStaleToken = |
| 2875 | | − existingRow?.tools_stale_token == null ? null : String(existingRow.tools_stale_token); |
| 2874 | + const observedStaleToken = existingRow?.tools_stale_token ?? null; |
| 2876 | 2875 | |
| 2877 | 2876 | // Does this attempt still own the connection? A read-path refresh takes |
| 2878 | 2877 | // a lease before dialing, and a slow one can lose it: another reader |
| @@ -2958,6 +2957,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 79 unmodified lines |
| 2958 | 2957 | * drift mark this listing answered — all three together, or none of |
| 2959 | 2958 | * them. Returns false when the attempt lost its lease. |
| 2960 | 2959 | * |
| 2960 | + * That atomicity is dialect-conditional: on D1 |
| 2961 | + * (`interactiveTransactions: false`) there is no transaction, the |
| 2962 | + * statements auto-commit one by one, and an interrupted run can leave a |
| 2963 | + * prefix of them applied. It still fails CLOSED, because the drift clear |
| 2964 | + * is the LAST statement: any prefix short of the whole leaves the token |
| 2965 | + * in place, so the connection stays drifted and re-lists. |
| 2966 | + * |
| 2961 | 2967 | * Already holds the persist permit, which is not reentrant: nothing in |
| 2962 | 2968 | * here may take it again. |
| 2963 | 2969 | * |
| @@ -3075,11 +3081,16 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 111 unmodified lines |
| 3075 | 3081 | // Reporting `incomplete` there would attribute a verdict to a row the |
| 3076 | 3082 | // winning attempt owns. |
| 3077 | 3083 | const recorded = yield* writeSyncOutcome(failureSet(kind, reason)); |
| 3084 | + // `recorded` rides along because the warning is emitted either way: a |
| 3085 | + // lost lease preserved the catalog too, but wrote no ladder entry. Without |
| 3086 | + // the field, a rate derived from these logs would count attempts the |
| 3087 | + // `incomplete` counter never saw. |
| 3078 | 3088 | yield* Effect.logWarning("executor tool sync preserved catalog", { |
| 3079 | 3089 | reason, |
| 3080 | 3090 | errorKind: kind ?? "unclassified", |
| 3081 | 3091 | integration: String(ref.integration), |
| 3082 | 3092 | connection: String(ref.name), |
| 3093 | + recorded, |
| 3083 | 3094 | }); |
| 3084 | 3095 | return yield* persistedTools(recorded ? "incomplete" : "lost_claim"); |
| 3085 | 3096 | } |
| @@ -3098,11 +3109,14 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 12 unmodified lines |
| 3098 | 3109 | // the plugin said nothing about why. Unclassified, so it backs off |
| 3099 | 3110 | // but never parks. |
| 3100 | 3111 | const recorded = yield* writeSyncOutcome(failureSet(null, reason)); |
| 3112 | + // `recorded` for the same reason as above: the warning fires even when |
| 3113 | + // the lease was lost and nothing was written. |
| 3101 | 3114 | yield* Effect.logWarning("executor tool sync preserved nonzero catalog", { |
| 3102 | 3115 | reason, |
| 3103 | 3116 | integration: String(ref.integration), |
| 3104 | 3117 | connection: String(ref.name), |
| 3105 | 3118 | existingToolCount: keptRows.length, |
| 3119 | + recorded, |
| 3106 | 3120 | }); |
| 3107 | 3121 | // Re-read on a lost lease: `keptRows` predates the write, and the |
| 3108 | 3122 | // attempt that owns the connection may have replaced the catalog |
| @@ -3946,10 +3960,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 837 unmodified lines |
| 3946 | 3960 | // with a new one is precisely how this signal outlives the listing that |
| 3947 | 3961 | // could not have seen it. |
| 3948 | 3962 | // |
| 3949 | | − // Locked: this is a lifecycle write like every other, and it can run while |
| 3950 | | − // a read's refresh fan-out holds an open BEGIN on the shared SQLite |
| 3951 | | − // connection — where a bare UPDATE is enrolled in that transaction and lost |
| 3963 | + // Locked: the permit serializes this write against the catalog-persist |
| 3964 | + // transactions specifically — `replaceCatalog` and `writeSyncOutcome`, the |
| 3965 | + // only permit holders that open a BEGIN. On the shared SQLite connection a |
| 3966 | + // bare UPDATE issued while one of those is open is enrolled in it, and lost |
| 3952 | 3967 | // with it if it rolls back. Losing THIS write loses a drift signal outright. |
| 3968 | + // The claim reaches no further: transactions in this file that do not take |
| 3969 | + // the permit are not serialized against this write. |
| 3953 | 3970 | const connectionsMarkToolsStale = (ref: ConnectionRef): Effect.Effect<void, StorageFailure> => |
| 3954 | 3971 | withCatalogPersistLock( |
| 3955 | 3972 | core.updateMany("connection", { |