Files
CodePilot/src/app/api/workspace/hook-triggered/route.ts
T
7418andClaude Opus 4.6 e2a0f49843 fix: CAS lock for assistant trigger, LIKE escape, provider options race
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>
2026-03-12 21:29:10 +08:00

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 });
}
}