Assistant trigger CAS (P1): - hook-triggered route: added expectedOwner param for compare-and-swap — rejects if current owner doesn't match, closing the race where two tabs both clear then both claim the lock - useAssistantTrigger: clear sends expectedOwner (stale session), set sends expectedOwner: null; non-2xx responses now bail instead of falling through - Server-side completion (chat/route.ts): only clears lock if owner matches current sessionId, preventing completed sessions from wiping active locks - Compensation branch: clear now passes expectedOwner to avoid bypassing CAS Image-gen LIKE escape (P2): - db.ts: escape backslashes before % and _ since ESCAPE '\' makes \ the escape char — prevents Windows paths / escaped JSON from corrupting the LIKE pattern and silently falling back to prompt-prefix matching Provider options fetch race (P2): - page.tsx + ChatView.tsx: provider options fetch now uses AbortController with cleanup on dependency change — prevents stale slow responses from overwriting the current provider's thinking/context_1m state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getSetting } from '@/lib/db';
|
|
import { loadState, saveState } from '@/lib/assistant-workspace';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const workspacePath = getSetting('assistant_workspace_path');
|
|
if (!workspacePath) {
|
|
return NextResponse.json({ error: 'No workspace path configured' }, { status: 400 });
|
|
}
|
|
const { sessionId, expectedOwner } = await request.json();
|
|
const state = loadState(workspacePath);
|
|
|
|
// Compare-and-swap: if expectedOwner is provided, only proceed when the
|
|
// current owner matches. This prevents two tabs that both see the same
|
|
// stale owner from racing through clear → set sequences.
|
|
if (expectedOwner !== undefined) {
|
|
const currentOwner = state.hookTriggeredSessionId ?? null;
|
|
if (currentOwner !== expectedOwner) {
|
|
return NextResponse.json({ success: false, reason: 'owner_mismatch', currentOwner });
|
|
}
|
|
}
|
|
|
|
// '__clear__' sentinel clears the field (used after onboarding/checkin completes)
|
|
if (sessionId === '__clear__') {
|
|
state.hookTriggeredSessionId = undefined;
|
|
state.hookTriggeredAt = undefined;
|
|
} else {
|
|
state.hookTriggeredSessionId = sessionId;
|
|
state.hookTriggeredAt = new Date().toISOString();
|
|
}
|
|
saveState(workspacePath, state);
|
|
return NextResponse.json({ success: true });
|
|
} catch (e) {
|
|
console.error('[workspace/hook-triggered] POST failed:', e);
|
|
return NextResponse.json({ error: 'Failed' }, { status: 500 });
|
|
}
|
|
}
|