Pointer map for the parallel build of docs/build/tasks/todo.md. Behavior is defined by docs/build/tasks/plan.md; this file fixes who owns which file and which names siblings import.
fm/sanctum-build-v1is the integration branch; every slice branches from it and merges back into it.- A slice edits only files it owns below, plus one registration line in each hot file it is listed against.
- Changes to another slice's files, tables or contracts go through the integration branch, never directly.
- A slice may amend migration statements for its own tables until the pull request merges; merged migrations may already be applied to the deployed database (operations.md), so they are never edited.
- When a sibling is not merged yet, build a stand-in at exactly its path and export name, headed
// stand-in: replaced by the <sibling> slice at integration, or use a test-side fake. - List every stand-in you created in your hand-off; integration keeps the real file and deletes the stand-in.
- Signatures below are the contract; extend them compatibly, never rename them.
- Run
npm run check:appand the gates in docs/CI.md before handing a slice back. - Report the Sentrux comparison results defined in docs/CI.md; a new cycle or import depth is an integration bug.
| Area | Where | Use |
|---|---|---|
| Wire schemas | packages/contracts/src | Branded IDs, UtcTimestamp, SourceRange, errors, every seam type; JSON-representable only. |
| Error envelope | errors.ts | Unauthenticated 401, Forbidden 403, NotFound 404, RevisionConflict 409, HashConflict 409, Unavailable 503. |
| HTTP contract | api.ts | SanctumApi from @sanctum/contracts/api only; the index never re-exports it, so registering a group never deepens modules that import the index. Each slice defines its HttpApiGroup in its contracts area file, importing only common.ts, errors.ts and auth.ts. |
| Live media | media.ts | Binary PCM frame layout, encodePcmFrame, decodePcmFrame, control messages. |
| Listener device API | capture.ts | ListenersApi (register, heartbeat, putChunk), LISTENER_STREAM_PATH. |
| Test audio | fixtures.ts | syntheticPcm from @sanctum/contracts/fixtures. |
| Config | server/src/config.ts | serverConfig, engineeringDefaults (plan 02), mcpAuthorizationConfig, requireActivation (rules in operations.md). |
| Database | server/src/db.ts | dbLayer, column schemas DbUtc, DbSafeInt, DbBool, DbJson, DbSha256, mysqlErrno. |
| Migrations | server/src/migrate.ts | Ledger, named lock, per-step resume; npm run migrate --workspace server. |
| Authorization seam | server/src/auth.ts | Authenticator tag, AuthenticatedLive; Authenticated middleware and CurrentAccess live in contracts. |
| Object storage | server/src/providers/object-store.ts | ObjectStore tag (put, head, get, presignGet, list, delete), ObjectStoreError.ambiguous and .unconfigured. |
| Job types | server/src/job-types.ts | ClaimedJob, JobOutcome, JobHandler<R>, JobHandlers<R>; imports no application module, so handler modules and jobs.ts never import the registry. |
| Job registry | server/src/job-handlers.ts | jobHandlers; only worker.ts imports it. JobFailure is in contracts. |
| Capture seam | web-app/src/lib/capture/view.ts | CaptureView, CaptureSnapshot, LevelSource, createCaptureStore. |
| Analyser levels | levels.ts | createAnalyserLevels(analyser, bandCount) for the waveform. |
| Server tests | server/tests/support | withDatabase, seedWorkspace, fixtureAccess, memoryObjectStore; one MySQL 8.4 per run. |
Conventions:
- IDs are UUID
CHAR(36) ascii_bin; wall clock isDATETIME(6)UTC written withUTC_TIMESTAMP(6); samples areBIGINT UNSIGNED. - Owned rows reference each other through
(workspace_id, id)foreign keys, so a row cannot point into another workspace. - Decode rows only through
SqlSchemaplus thedb.tscolumn schemas; never readDateorNumberBIGINTs from mysql2. - Domain functions take
access: AccessScopefirst and never read credentials; unauthorized reads fail asNotFound. - Tests use synthetic fixtures and fakes; no live provider, R2 or Pipedream call from any test.
server/src/providers/never imports application modules; providers take options, and application code imports providers, never the reverse (a two-way directory dependency tripled Sentrux coupling).
Each entry lists owned files, then the exact exports siblings import. R is SqlClient.SqlClient unless stated.
Owns server/src/store.ts, server/src/auth.ts (extends the foundation file), server/src/agents.ts, server/src/cache.ts, server/src/jobs.ts (replaces the stand-in), migrations 001_initial and 004_jobs, and the agent section of packages/contracts/src/auth.ts (kept beside SessionApi so no import chain deepens).
Kernel added browser_sessions.workspace_id to 001_initial and jobs.rearmed (coalesced re-arm while running) to 004_jobs.
auth.ts:KernelAuthenticatorLive: Layer<Authenticator, never, R>(sessions + hashed bearer credentials) is main.ts's default; the unconfigured authenticator is gone.auth.ts:resolveAccess(input: { workspace_id; principal_id }): Effect<AccessScope, Forbidden, R>for workers and sockets.auth.ts:requireScope(access, scope: AccessScopeName): Effect<void, Forbidden>.auth.ts:authorizeMeeting(access, meeting_id, need: 'read' | 'write'): Effect<void, NotFound, R>.auth.ts:listVisibleMeetingIds(access): Effect<ReadonlyArray<MeetingId>, SqlError, R>.jobs.ts:enqueueJob(input: EnqueueJob): Effect<JobId, SqlError, R>, joining the caller's transaction.jobs.ts:EnqueueJob = { workspace_id; kind: JobKind; work_key: string; payload: unknown; requested_by: PrincipalId | null; source_revision?: number; delay_ms?: number; max_attempts?: number; expedite?: boolean }; an active row with the same key is re-armed (timer restarts;expeditemakes it due no later than this request).job-runner.ts:runWorker<R>(handlers: JobHandlers<R>, options?): Effect<never, SqlError, R | SqlClient>, plus claim, lease, fenced completion and deadlock retry; its sweeper also runs media'ssweepLapsedListenersfrom listeners.ts and meetings'sweepIdleMeetingsfrom meetings.ts. Only worker.ts imports it.jobs.tsholds onlyenqueueJoband imports no application module, so enqueueing never deepens a caller's import chain.db.ts:nextContextSeq(workspace_id): Effect<number, SqlError, R>locks the workspace row; call inside the change's transaction.store.ts:bumpPermissionRevision(workspace_id): Effect<number, SqlError, R>.store.ts:workspaceForOrg(input: { issuer; org_id }): Effect<Option<WorkspaceId>, SqlError, R>andlinkWorkspaceOrg(input: { workspace_id; issuer; org_id }): Effect<void, SqlError, R>(idempotent; a clash with another link is a defect) over migration012_workspace_orgs.store.ts:addMember(input: { workspace_id; principal_id; role; org_issuer? })storesworkspace_members.org_issuer(migration016_member_org_issuer): the issuer whose organization sync granted the membership, NULL when Sanctum did; org sync revokes only memberships its issuer granted.store.ts:setMembership(issuer, workspace_id, principal_id, role | null): Effect<void, SeatLimitReached | SqlError, R>, the upsert (throughaddMemberand its seat limit) and revoke path that provider organization glue calls;roleFromSlugs(slugs)maps organization role slugs to a Sanctum role;claimSeat(workspace_id, principal_id | null)checks a seat in its own transaction.auth.tscreateHumanPrincipal({ issuer, subject, name })creates the human principal and its identity link for the same glue.cache.ts:scopedCacheKey(access, ...parts: ReadonlyArray<string | number>): stringincluding principal, permission and source revisions.- contracts:
AgentsApiwithcreateAgent,listAgents,revokeCredential.
Owns web-app/src/lib/capture/{controller,permissions,microphone,recorder,recording-worklet,buffer,uploader,orphans}.ts, web-app/src/pages/listen/engine.ts, their tests.
engine.ts:getCaptureEngine(): CaptureView, a singleton above every overlay/router lifecycle;subscribeTranscript(listener)andsubscribeActions(listener)fan out live transcript segments andaction_updatemessages from the listener stream.controller.ts:createCaptureController(deps): CaptureView & { dispose(): void }, publishing throughcreateCaptureStore.- Client of
ListenersApi,LISTENER_STREAM_PATH,StartMessage,encodePcmFrameandRecordingChunkManifest. - Hands group ownership to serve through the
HeartbeatReceipt.ownerflag only.
Owns web-app/src/pages/listen/{index.tsx,waveform.ts,rails.ts,captions.ts,listen.css,Dialog.tsx,MenuPanel.tsx,Fullscreen.tsx,MenuSearch.tsx,MeetingsSection.tsx,ReviewSection.tsx,InputPicker.tsx,input-level.ts}, web-app/src/main.tsx, web-app/e2e/listen-*.spec.ts.
Dialog.tsx:Dialog({ title, open, onClose, children, closeLabel?, className? })with focus trap, Escape and focus return; MenuPanel and the welcome (closeLabelSkip) reuse it.Fullscreen.tsx:useFullscreen()returns[fullscreen, toggle](the Settings > Display row and the F key share it;useFullscreenKey(toggle)binds the F key);useIdle(active, ms)is true aftermswithout input whileactive, andListenPageuses it with fullscreen to fade the footer.MenuPanel.tsx:MenuPanel({ open, onClose, sections, active, onSelect, client, commands, onMeeting }), the one central overlay opened by the footer's Menu button or Ctrl/⌘+K: a rail ofMenuSection { id, label, body }beside the active section's body. Adding a section is one more entry inListenPage'ssectionsarray (a component such asMeetingsSection.tsxthat takesclientand renders no dialog of its own); a section's body mounts only while shown.MenuSearch.tsx: the search box at the top of the Menu; while it holds text it replaces the rail and section. It lists decisions and commitments fromGET /api/v1/context/search, meeting titles from the newest 200 meetings,Open <section>and thecommandsListenPage passes (Fullscreen, Sign out). ↑/↓ and Enter choose; Escape clears the box before it closes the Menu. A meeting result setsMeetingsSection'srequest(a new{ id }per choice, so choosing the same meeting again reopens it), which opens that meeting's Review beside the list, or says it could not be opened. There is no transcript full-text search.InputPicker.tsx:InputPicker({ engine, check? }), the microphone choice (throughCaptureView.chooseInput); only withcheckdoes it also test the chosen input for sound while capture is stopped or paused;InputCheckText({ input })is that test's line for oneuseInputLevelreading (a silent built-in input, label/built-in|macbook/i, adds a closed-laptop hint); the picker has a Refresh button and re-reads the input list ondevicechange;input-level.ts:useInputLevel(deviceId), the live level and dead-input state of one input outside capture. Settings and the silent-input warning use the picker, and only Settings setscheck; the welcome's microphone step uses the picker withoutcheckand draws its level bar andInputCheckTextfrom oneuseInputLevelreading. The dead-input floor, threshold and run rule (SILENT_PEAK,SILENT_SECONDS,deadRun) live once in capture'smicrophone.ts, shared by the capture engine and the check.waveform.ts:startWaveform(canvas, levels, listener), the kiosk orb canvas ported one-to-one; samples never enter React state.rails.ts:startTranscriptRail(lines, subscribeTranscript)andstartActionFeed(feed, subscribeActions), the kiosk's side live updates.- Imports only
getCaptureEngine,subscribeTranscript,subscribeActions, view.ts types andmicrophone.ts(light: no recorder) from capture; compares againstdesign/listener-reference.svgat 1280x720 and a narrow laptop size. MeetingsSection.tsx: the Menu's first section, a pagedlistMeetingslist (live meeting under Now) beside the chosen meeting's Review;ReviewSection({ client, meeting })shows its six tabs andloadReview(client, meeting, signal, update)reads their sources.- Review tabs (T21 box 2) live in
ReviewPanels.tsxandreview-data.ts. They added three compatible API fields:GET /meetings/{id}/actions(listMeetingActionsinActionsApiand actions.ts), an optionalmeeting_idonGET /context/changes, andpiecesplussample_rateonRecordingAccess(playback.ts) to map a source sample to a playback offset.
Owns server/src/matcher.ts, server/src/llm.ts, server/src/planner.ts, server/src/extraction.ts, server/src/providers/{workers-ai,anthropic,openai,cerebras}.ts, migration 009_matching, scripts/benchmark-matching.ts, and adds modelRoles to config.ts.
llm.ts:LlmClienttag,LlmLive: Layer<LlmClient, ConfigError>,fixtureLlm(responses)for tests; missing keys fail withUnavailable.extraction.ts:extractCandidates(input: { meeting: Meeting; segments: ReadonlyArray<TranscriptSegment>; snapshot: ReadonlyArray<ContextItem>; known?: ReadonlyArray<Pick<ContextItem, 'kind' | 'text'>>; epochs?: ReadonlyArray<EpochAnchor> }): Effect<ReadonlyArray<ExtractionCandidate>, Unavailable, LlmClient>;snapshotis the caller's relevant items (context'srelevantItems), fitted to a 4,000-character prompt budget;knownis every current item's kind and text (context'smeetingItemTexts), so a repeat of any current item is rejected; failsUnavailablewhen a segment's epoch has no anchor (passcapture_epochsanchors).extraction.ts:summarizeMeeting(input: Omit<ExtractionInput, 'snapshot'>): Effect<MeetingNotes, Unavailable, LlmClient>, the one canonical summary for notes, email sections and exports; it summarizes the transcript in windows of 120 lines (3 requests at once) and, for more than one window, writes the title and summary from the window summaries, so output per request stays belowmodelRequest.maxOutputTokensfor any meeting length.planner.ts:planWork(access, input: { meeting_id; request: string; actions: ReadonlyArray<GetIntegrationActionOutput>; research?: { text: string; sources; planned: ReadonlyArray<RequestActionInput>; job_id: string } }): Effect<{ web_research: boolean; actions: ReadonlyArray<RequestActionInput>; rejected?: string }, Unavailable | Forbidden, LlmClient>; offers only inspected actions whose missing fields it can fill and never theappfield;rejectednames each dropped proposal and why; a plan that asks for web research may leave required content fields for the second pass; withresearch, research may change only content fields of the planned actions, each keeps its planned title and gets an idempotency key fromjob_id, its non-content arguments and its position among planned actions with the same ones, and any other change returnsrejectedwith no actions. See actions for retry and receipt rules.llm.ts:research(prompt, admit?, record?)sends one hosted web-search request (OpenAI Responsesweb_searchby default) afteradmit(model)succeeds, never retries it in-process, and passes the provider-reported usage torecord, also when a billed call then fails.planner.ts:respondToRequest(input: { request: string; context: ContextSnapshot }): Stream<string, Unavailable, LlmClient>for requested speech.matcher.ts:rankMatches(access, query: { profile_id; kind: 'needs' | 'offers'; top_k: number }): Effect<ReadonlyArray<{ profile_id: ProfileId; score: number }>, NotFound, R>.- Matching (integration-owned):
MatchingApi(contractsmatching.ts,GET /api/v1/profiles/{profile_id}/matches) and thematching.rankjob live in servermatching.ts; both callrankMatchesafter acontext:readcheck, and the job stores its ranking as a workspacedocumentartifact.
Owns server/src/providers/pipedream.ts, server/src/integrations.ts, server/src/integration-accounts.ts, web-app/src/pages/listen/AppsSection.tsx, the integration_accounts statement in 008_actions, IntegrationsApi and IntegrationAccountsApi in packages/contracts/src/integrations.ts.
providers/pipedream.ts:PipedreamClienttag,PipedreamLive,fixturePipedream(catalog)(10,000-entry fixture support).integrations.ts:searchIntegrationActions(access, input: SearchIntegrationActionsInput): Effect<SearchIntegrationActionsOutput, Unavailable, R | PipedreamClient>.integrations.ts:getIntegrationAction(access, input: GetIntegrationActionInput): Effect<GetIntegrationActionOutput, NotFound | Unavailable, R | PipedreamClient>.integrations.ts:executeIntegrationAction(input: { access; account_id; action_key; version; configuration_ref; arguments; provider_idempotency_key }): Effect<{ receipt: Record<string, unknown> }, IntegrationFailure, R | PipedreamClient>;IntegrationFailure.ambiguousmarks unknown outcomes.integration-accounts.ts:IntegrationAccountsLive, the Menu Apps section's website-only connect flow (/api/v1/integrations/connect,/integrations/accounts,/sync,/{id}/disconnect); the Pipedream external user is<workspace_id>.<principal_id>from the session, never client input. Sync upserts the caller's Pipedream accounts, stores accounts Pipedream reports dead or no longer lists as disconnected, and grants nothing.
Owns server/src/listeners.ts, server/src/recordings.ts, server/src/transcripts.ts, server/src/media/{ingest,session}.ts, server/src/providers/{whisper,r2}.ts, migrations 002_capture and 003_transcripts, media.ts and ListenersApi in contracts.
- Registers
ListenersApiin contracts api.ts and its handlers in server api.ts; attaches the upgrade handler in main.ts. providers/r2.ts:R2ObjectStoreLive: Layer<ObjectStore, ConfigError>; worker.ts provides it (an unconfigured stand-in until media lands).recordings.ts:listCommittedChunks(input: { workspace_id; source: SourceRange }): Effect<ReadonlyArray<RecordingChunkManifest & { object_key: string }>, SqlError, R>.transcripts.ts:finalSegments(access, source: SourceRange): Effect<ReadonlyArray<TranscriptSegment>, SqlError, R>.transcripts.ts:getSegments(access, ids: ReadonlyArray<TranscriptSegmentId>): Effect<ReadonlyArray<TranscriptSegment>, SqlError, R>for source validation.session.tscalls meetings' hooks and actions'SpeechGate; handles job kindtranscript.reconcile.
Owns server/src/meetings.ts, server/src/meeting-store.ts, server/src/meeting-corrections.ts, server/src/meetings-api.ts, server/src/boundaries.ts, server/src/playback.ts, server/src/speakers.ts, server/src/providers/pyannote.ts, scripts/evaluate-speakers.ts, migrations 005_meeting_ranges, 006_speakers and 017_meeting_end_fence, MeetingsApi in contracts meetings.ts.
Plan T13 lists recordings.ts; its playback half lives in playback.ts so media keeps recordings.ts.
meetings.ts:onFinalSegments(event: { workspace_id; listener_id; capture_group_id: string | null; segments: ReadonlyArray<TranscriptSegment> }): Effect<void, SqlError, R>.meetings.ts:onCaptureEnded(event: { workspace_id; listener_id; epoch_id; track; sample_end; reason: EpochEndReason }): Effect<void, SqlError, R>.meetings.ts:sweepIdleMeetings(idleMs?): Effect<void, SqlError, R>closes open meetings that are quiet forengineeringDefaults.meetingIdleCloseMs(see DECISIONS.md, "Meeting end decision").meetings.ts:getMeeting(access, meeting_id): Effect<Meeting, NotFound, R>andmeetingRanges(access, meeting_id): Effect<ReadonlyArray<MeetingRange>, NotFound, R>.playback.ts:issueRecordingAccess(access, meeting_id): Effect<RecordingAccess, NotFound | Forbidden | Unavailable, R | ObjectStore>.- Handles job kinds
meeting.finalize,recording.assemble,speakers.refine; boundary corrections call context'sappendContextEvent. MeetingsApioperations:listMeetings,getMeeting,closeMeeting,endMeeting,splitMeeting,mergeMeetings,getTranscript,recordingAccess,mapSpeaker,getNotes,exportMeeting.- Notes (integration-owned):
meeting.finalizeenqueuesnotes.summarize(notes-job.ts), which stores the models slice's canonicalsummarizeMeetingoutput onmeetings.noteswithnotes_revisionandprocessing.notes; corrections, and final text that lands after a close left the transcript incomplete (recordFinalWindow), re-finalize and so regenerate notes.notes.tsservesgetNotesand the MarkdownexportMeeting;meeting-evidence.tsloads a meeting's final segments and epoch anchors for extraction and notes. - Automatically detected meetings start restricted;
createMeetinggrantsowneronly to the capturing listener's principal (DECISIONS.md), and kernel'sauthorizeMeetinghas no role override.
With no open meeting, a nonblank final without an existing or fenced owner opens a provisional conversation, even for one-word speech.
Earlier finals bypass forward boundary scoring; the latest owned source position and boundary cues stay current across capture epochs.
Speech before the meeting start can extend an adjacent range in the same epoch and track, unless another owner or End fence intervenes.
Otherwise, while a later conversation stays open, recovered speech gets separate restricted ownership without replacing that conversation.
Adjacent recovered finals can share this recovered conversation in either delivery order while the later conversation stays open.
Forward extension of recovered ownership excludes explicit closes, split/merge corrections, and End fences.
Promotion uses the latest owned end, not the arrival order.
The regression cases live in server/tests/ingest.test.ts and server/tests/meeting-boundaries.test.ts.
Recording assembly stores each WAV at meetings/<workspace>/<meeting>/r<revision>-<sha256>.wav, with the WAV hash in the key.
Overlapping attempts cannot overwrite different audio at the same key.
Before publishing, assembly locks the meeting row and checks the revision and current ranges against the cut.
A changed revision or range causes a retryable failure; playback and speaker refinement use the published meeting_recordings.object_key.
Under that lock, assembly preserves the first published cut and its status, even if another attempt finds more audio or no valid audio.
Owns server/src/context.ts, server/src/context-events.ts, server/src/context-changes.ts, server/src/context-jobs.ts, server/src/context-schedule.ts, migrations 007_context and 023_context_text_joined, ContextApi in contracts context.ts.
context-events.ts:appendContextEvent(input: { workspace_id; meeting_id: MeetingId | null; item: { id; revision } | null; change: ContextChangeKind; actor: PrincipalId; source_revision?: number }): Effect<number, SqlError, R>, inside the caller's transaction.context.ts:getContextSnapshot(access, meeting_id): Effect<ContextSnapshot, NotFound, R>andaddContextItem(access, input: AddContextItem): Effect<ContextItem, RevisionConflict | HashConflict | NotFound, R>.- Handles job kinds
context.refreshandmemory.commitusingextractCandidates. context-schedule.ts:requestLiveContextRefresh(workspace_id, meeting_id)counts the meeting's unprocessed final segments and callsrequestContextRefreshwithrequested_by: null(system actor); meetings'onFinalSegmentscalls it after it claims new speech for the open meeting, so context updates during the meeting, not only atmemory.commit.ContextApioperations:getContext,searchContext,addContextItem,reviseContextItem,getContextChanges,getSource.
Owns server/src/actions.ts, server/src/executor.ts, server/src/research.ts, server/src/media/speech-gate.ts, server/src/speech-requests.ts, server/src/providers/speech.ts, web-app/src/lib/capture/playback.ts, the grant and action statements in 008_actions, migrations 010_action_titles, 021_paid_model_calls and 022_action_research_job, ActionsApi in contracts actions-api.ts (registered through api.ts only, never the index), speech control messages in contracts media.ts.
actions.ts:requestAction(access, input: RequestActionInput, research_job_id?: JobId | null): Effect<RequestActionOutput, Forbidden | NotFound | HashConflict, R>(the third gateway);research_job_idis written with the action.actions.ts:getActionReceipt(access, action_id): Effect<ActionReceipt, NotFound, R>.speech-gate.ts:SpeechGatetag withopenRequest({ listener_id; epoch_id; request_id; sample_end }),mayEmit(request_id, generation): boolean,cancel(listener_id, reason).playback.ts(web):createPlayback(context: AudioContext)registered by engine.ts; drops chunks of cancelled generations.- Handles job kinds
action.execute,action.reconcile,research.run. actions.ts:listenerFeed(access, listener_id)returns the listener's open meeting and its agent-work feed rows (newestACTION_FEED_ROWS, oldest first) with the readable title chosen in one place: the request's optionaltitle, else a label made fromaction_key.- Media's session sends them as
action_updateon the listener stream: a snapshot after eachaccepted, then changes, read from MySQL everyliveLimits.actionFeedMsbecause the job worker that changes most states runs in another process. speech-gate.ts:speechController(...)per live socket and theSpeechRepliestag; media's session creates the controller only when the process providesSpeechSynthesizer(media/providers.tsSpeechSynthesizerLive) andSpeechReplies(speech-requests.tsSpeechRepliesLive), so a process without them stays silent.speech-requests.ts: the API providesSpeechWorkRequestsLive; session.ts passes this optional service to the controller, which runs it in its own daemon fiber before speaking. It resolvestruewhen research was enqueued or already exists for the request; the controller then speaksWORK_ACKNOWLEDGEMENTinstead of the reply. The spoken work decision owns the trigger and authorization contract. The classifier provider decision owns model selection and planner fallback.- Replies read the requester's context for the listener's open meeting; a device credential without
context:readgets no reply, never a guess. - Results over the Pipedream output budget are stored as
action_outputartifacts and referenced byprovider_receipt.artifact_id. research.run(executor.tswithresearch.ts): finds the requester's granted actions among thesearchIntegrationActionsmatches for the request in each granted app (one search per app, so other usable apps cannot crowd them out), inspects each with its grant's account, letsplanWorkdecide on web research and fill in actions, researches first, then requests each planned action throughrequestAction, which writes the job ID toactions.research_job_idwith the action. Before requesting the first action, it saves every planned request, including its idempotency key, injobs.payload.planned_requests. It saves the offered actions, research references, and reasons for no actions injobs.payload.research_resultin the same update, including empty plans. Retries and overlapping attempts use the first saved plan and its result metadata, request each missing action, and report every planned action. Retries call the sharedrequestActiongateway to check request identity before reporting an existing action asalready_requested, without submitting it again. A reused key with a different request produces an action result withrefused: 'HashConflict', not the earlier action. Recovery reads action records before checking for a saved plan, so a concurrent plan takes precedence over the action-only fallback. If an older job has action records but no saved plan, the retry reports those records without planning again. Unstored requests cannot be recovered. A retried job with stored research but no requested actions uses that research without another paid call, even when its new plan declines web research. When research is available and actions are planned,planWorkruns again over only those actions, with cited research in a delimited untrusted block. Research may fill only content fields (subject, body, notes and the like). A pass that changes another argument, adds an action or drops one is rejected. Rejected passes and refused research produce empty candidate plans, with reasons inresearchoroutcome. These candidates cannot replace an earlier saved plan. Web research reserves one of the workspace'sSANCTUM_PAID_RESEARCH_CALLS_PER_DAYand the install'sSANCTUM_PAID_RESEARCH_CALLS_PER_DAY_TOTALpaid calls inpaid_model_callsbefore sending, records the provider's token usage (also for a billed call that ends incomplete), stores aresearchartifact once per job, and adds anexternalresearch_observationciting it; a spent allowance is arefusedresult, a rate limit pauses the job, a dropped proposal is reported indroppedwith its reason, and nothing to do is a reportedoutcome.- Pending live finals are deduplicated by ID and ordered by source samples before source-gap boundaries split requests.
A gap of at least
engineeringDefaults.speech.endOfTurnMsseparates turns. Coverage completes each turn once it extends that far past its end, even while later speech remains pending. A newer lane's coverage waits until older lanes finish flushing. Without sufficient coverage,engineeringDefaults.speech.turnWaitMswithout new finals completes pending turns. Later finals never merge into a completed request. Seen final IDs remain ignored for the socket's lifetime; unseen earlier speech can interrupt a reply and start a request. Regression cases:server/tests/asr-lane-order.test.tsandserver/tests/silence.test.ts.
Owns server/src/mcp.ts, scripts/generate-sdks.ts, sdk/typescript/, sdk/python/, sdk/examples/, web-app/src/pages/listen/AgentsSection.tsx, web-app/src/pages/listen/AgentWork.tsx, web-app/src/pages/listen/AccessTokens.tsx, server/tests/api-contract.test.ts, server/tests/mcp.test.ts.
- Generates OpenAPI from
SanctumApi; mounts/mcpfrom main.ts. - MCP tools:
list_meetings,get_context,search_context,get_source,get_context_changes,add_context,revise_context,request_action,get_action,search_integration_actions,get_integration_action. - Tools call the functions named above; no business schema is redefined.
Owns server/Dockerfile, docker-compose.yml, Caddyfile, production parts of web-app/vite.config.ts, server/src/capture-groups.ts, server/tests/handoff.test.ts, server/tests/capabilities.test.ts.
capture-groups.ts:claimGroupLease(input: { workspace_id; capture_group_id; listener_id }): Effect<boolean, SqlError, R>, called inside media's heartbeat, andholdsGroupLease(workspace_id, listener): Effect<boolean, SqlError, R>, required by livestartand watermark writes.- Serves built web assets from main.ts behind the API routes.
Owns deploy/cloudflare/ (Worker, Container classes, wrangler.jsonc) and the Cloudflare section of operations.md; runs the server/Dockerfile image unchanged.
Owns web-app/landing/ (static page, npm run build:landing), web-app/e2e/landing.spec.ts and the landing branch of the Worker in deploy/cloudflare/src/index.ts; operation in operations.md.
- The hero runs the listening view's
startWaveformfromweb-app/src/pages/listen/waveform.tson simulated levels and never asks for the microphone; reduced motion keeps one still frame. - The page loads only its own built files; the Worker adds its CSP, and the copy states only what the hosted site runs today.
Owns server/src/signin.ts, server/src/owner.ts, server/tests/signin.test.ts; operation in operations.md.
- Mounts
/auth/*from main.ts; opens sessions throughopenSessionandlinkIdentityin auth.ts. - Login refreshes
principals.display_nameandprincipals.email(migration018_principal_email) from the ID token (name, elsegiven_nameandfamily_name, with an email but no name the display name is cleared to NULL); Settings' Sign-in row shows them (the email leads when the name is NULL; shared and audit text then says "a member") with the role as a label and initials for the avatar. - B1 (self-hosted organizations):
server/src/issuer-orgs.ts(Better Auth organization hooks and the sign-in repairreconcileMember), migration013_issuer_organizations,web-app/src/pages/auth/team.tsx(Team and Profile dialogs) and the/invite/<id>page; operation in operations.md.
Owns server/src/org-sync.ts, server/src/providers/workos.ts and the WorkOS suites in server/tests/signin.test.ts; operation in operations.md.
org-sync.ts:reconcileSignIn(identity, name, create), called by signin.ts after the ID token is verified;linkExistingWorkspace(settings, workspace_id, identity)for Team's "Set up team";syncWorkosEventshandles job kindworkos.syncand is registered in worker.ts next to its layer (one more import would make job-handlers.ts a Sentrux god file);armWorkosSyncschedules it when the worker starts.WorkosOrganizationstag (WorkosOrganizationsFromEnv), provided by main.ts and worker.ts.
Owns server/src/workspaces.ts, migration 014_workspace_deletion, WorkspaceApi and the WorkspaceOwner middleware in contracts workspace.ts, web-app/src/pages/listen/WorkspaceDeletion.tsx.
auth.tsjoins only live workspaces, so a deleted workspace refuses every session, credential andresolveAccessat once;WorkspaceOwnerLiveandopenSessionalone still admit an owner untilpurge_after, so the owner can sign in again and undo.- Deleting ends this process's open listener sockets of the workspace with a
rejectedmessage (unauthorized) and close code 1008 (server/src/media/open-sockets.ts), andworkspaceIsLivein store.ts refuses segment, chunk commit and recording-object writes of a deleted workspace in every process; a reconcile or recording job that meets the deletion mid-run fails with the requester refusal, so Undo requeues it; the web app pauses capture after a successful delete. - Handles job kind
workspace.purge; a table added with aworkspace_idcolumn joinsPURGED_TABLESin workspaces.ts, children first. The purge also deletes the embedded issuer's organization of the workspace (auth_invitation,auth_member,auth_organizationwithid = workspace_id, which hold noworkspace_idcolumn) and clearsauth_session.activeOrganizationIdthat names it. It keeps the deployment-wideworkos.syncjob row, because it is anchored to the oldest workspace.
Owns server/src/widget-token.ts, server/tests/support/team-server.ts, web-app/src/pages/listen/{WorkosTeam.tsx,team.css}, web-app/e2e/team-csp.spec.ts and the widget-token suite in server/tests/signin.test.ts; operation in operations.md.
widget-token.ts:WidgetTokenLivemountsGET/POST /api/v1/workspace/team(link status; owner-only "Set up team") andPOST /api/v1/workspace/widget-tokenfrom main.ts; it callsWorkosClient.widgetTokeninproviders/workos.ts./auth/configreportsworkos_organizations; SettingsSection lazy-loadsWorkosTeam.tsxfor admins of a linked workspace and owners on such a server, and shows it inline on the Workspace row.- The
style-srchashes inweb.tscover the widgets' fixed<style>elements;team-csp.spec.tsfails when a dependency bump changes one.
Owns server/src/onboarding.ts, migration 020_onboarding, OnboardingApi in contracts onboarding.ts, server/tests/onboarding.test.ts, web-app/src/pages/listen/Welcome.tsx and web-app/e2e/listen-welcome.spec.ts.
OnboardingApiis website only (OpenApi.Exclude, so not in the SDKs or MCP):GET/POST /api/v1/onboardingread and setprincipals.onboarded_atwith the workspace name, andPOST /api/v1/workspace/namerenames the workspace forworkspace:admin.TeamButtonin SettingsSection.tsx opens this server's Team (self-hosted issuer, WorkOS widgets or Set up team) in a dialog over the welcome; Settings shows the sameTeamBodyinline; the microphone step uses listen-ui'sInputPicker,InputCheckTextanduseInputLevel.
| File | Who touches it | How |
|---|---|---|
packages/contracts/src/api.ts |
every slice with REST | One .add(XApi) line. |
server/src/api.ts |
every slice with REST | One handler layer in the ApiLive list. |
server/src/main.ts |
kernel, media, interfaces, serve, sign-in, team widgets | Authenticator swap; upgrade handler; /mcp mount; /auth/* mount; widget-token mount; static assets. |
server/src/worker.ts |
slices with provider layers | Provide the layer next to dbLayer. |
server/src/job-handlers.ts |
media, meetings, context, actions | One kind: handler entry; the services a handler needs come from its type, and worker.ts provides their layers. Handler modules import job-types.ts, never this file. |
server/src/config.ts |
models, media, actions, pipedream | Own key inside serverConfig; defaults stay in engineeringDefaults. |
server/src/media/session.ts |
media, meetings, actions | Media owns it; siblings expose functions it calls. |
web-app/src/pages/listen/engine.ts |
capture, listen-ui, actions | Capture owns it; actions adds playback registration. |
packages/contracts/src/jobs.ts |
any slice adding a job kind | Add the literal to JobKind. |