feat(M13): get_worker_settings + worker_heartbeat tools (v0.7.0)

T-519 — pre-flight quota-gate voor de worker-loop.

Twee nieuwe MCP-tools:
- get_worker_settings (read): retourneert User.min_quota_pct. Worker
  roept dit elke iteratie aan vóór de quota-probe.
- worker_heartbeat (write): worker rapporteert last_quota_pct +
  last_quota_check_at na een probe. Update ClaudeWorker en emit
  pg_notify 'worker_heartbeat' op scrum4me_changes-channel zodat
  NavBar stand-by-badge real-time updatet. requireWriteAccess
  (demo-blok).

Schema-resync: vendor/scrum4me bijgewerkt naar 555ed8f waarmee de
M13-velden (User.min_quota_pct, ClaudeWorker.last_quota_pct +
last_quota_check_at) beschikbaar zijn voor Prisma client.

Bestaande achtergrond-heartbeat (presence/heartbeat.ts, 5s tick op
last_seen_at) blijft ongewijzigd. Worker_heartbeat is een aparte
expliciete call met quota-info.

Versie naar 0.7.0 (minor — twee nieuwe tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-06 04:23:31 +02:00
parent b48f2a5c74
commit d50075d960
6 changed files with 192 additions and 8 deletions

View file

@ -28,6 +28,8 @@ import { registerGetIdeaContextTool } from './tools/get-idea-context.js'
import { registerUpdateIdeaGrillMdTool } from './tools/update-idea-grill-md.js'
import { registerUpdateIdeaPlanMdTool } from './tools/update-idea-plan-md.js'
import { registerLogIdeaDecisionTool } from './tools/log-idea-decision.js'
import { registerGetWorkerSettingsTool } from './tools/get-worker-settings.js'
import { registerWorkerHeartbeatTool } from './tools/worker-heartbeat.js'
import { registerImplementNextStoryPrompt } from './prompts/implement-next-story.js'
import { getAuth } from './auth.js'
import { registerWorker } from './presence/worker.js'
@ -89,6 +91,9 @@ async function main() {
registerUpdateIdeaGrillMdTool(server)
registerUpdateIdeaPlanMdTool(server)
registerLogIdeaDecisionTool(server)
// M13: worker quota-gate tools
registerGetWorkerSettingsTool(server)
registerWorkerHeartbeatTool(server)
registerImplementNextStoryPrompt(server)
// Presence bootstrap MUST run before server.connect — the stdio transport

View file

@ -0,0 +1,33 @@
// MCP read-tool: lees de worker-instellingen van de geauthenticeerde user.
//
// Worker roept dit aan vóór elke wait_for_job iteratie zodat hij weet
// wanneer hij stand-by moet (pre-flight quota-gate).
//
// Auth: api-token; user_id afgeleid uit token. Demo mag.
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { prisma } from '../prisma.js'
import { getAuth } from '../auth.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
export function registerGetWorkerSettingsTool(server: McpServer) {
server.registerTool(
'get_worker_settings',
{
title: 'Get worker settings',
description:
'Read the authenticated user\'s worker settings (min_quota_pct). Worker should call this each iteration before doing the pre-flight quota probe.',
inputSchema: {},
},
async () =>
withToolErrors(async () => {
const auth = await getAuth()
const user = await prisma.user.findUnique({
where: { id: auth.userId },
select: { min_quota_pct: true },
})
if (!user) return toolError('User not found')
return toolJson({ min_quota_pct: user.min_quota_pct })
}),
)
}

View file

@ -0,0 +1,81 @@
// MCP write-tool: worker rapporteert quota-pct na pre-flight probe.
//
// Aanvulling op de bestaande achtergrond-heartbeat (die alleen last_seen_at
// elke 5s tickt). Deze tool wordt expliciet aangeroepen door de worker
// nadat scripts/worker-quota-probe.sh een quota-meting heeft gedaan.
//
// Updates ClaudeWorker.{last_quota_pct, last_quota_check_at, last_seen_at}
// en emit een pg_notify-event op 'scrum4me_changes' zodat de UI de
// stand-by-badge real-time kan tonen.
//
// Auth: api-token; demo mag niet (worker is geen demo-flow).
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { Client } from 'pg'
import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
const inputSchema = z.object({
last_quota_pct: z.number().int().min(0).max(100),
last_quota_check_at: z.string().datetime().optional(),
})
export function registerWorkerHeartbeatTool(server: McpServer) {
server.registerTool(
'worker_heartbeat',
{
title: 'Worker heartbeat with quota',
description:
'Report the worker\'s most recent rate-limit quota percentage to the server. Updates ClaudeWorker.last_quota_pct + last_quota_check_at. Emits a SSE event so the UI can show stand-by status. Forbidden for demo accounts.',
inputSchema,
},
async ({ last_quota_pct, last_quota_check_at }) =>
withToolErrors(async () => {
const auth = await requireWriteAccess()
const checkAt = last_quota_check_at ? new Date(last_quota_check_at) : new Date()
const result = await prisma.claudeWorker.updateMany({
where: { token_id: auth.tokenId },
data: {
last_seen_at: new Date(),
last_quota_pct,
last_quota_check_at: checkAt,
},
})
if (result.count === 0) {
return toolError(
'Worker record not found — call register_worker first or wait for the next heartbeat tick',
)
}
// pg_notify zodat NavBar realtime kan updaten. Failure is non-fatal:
// de DB-write is al gebeurd, alleen de live-update mist dan.
try {
const pg = new Client({ connectionString: process.env.DATABASE_URL })
await pg.connect()
await pg.query('SELECT pg_notify($1, $2)', [
'scrum4me_changes',
JSON.stringify({
type: 'worker_heartbeat',
user_id: auth.userId,
token_id: auth.tokenId,
last_quota_pct,
last_quota_check_at: checkAt.toISOString(),
}),
])
await pg.end()
} catch {
// non-fatal
}
return toolJson({
ok: true,
last_quota_pct,
last_quota_check_at: checkAt.toISOString(),
})
}),
)
}