@agent-native/core 0.111.4 → 0.113.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +4 -2
- package/corpus/core/CHANGELOG.md +34 -0
- package/corpus/core/docs/AGENTS.md +7 -0
- package/corpus/core/docs/content/integrations.mdx +63 -2
- package/corpus/core/docs/design/provider-integration-authority.md +117 -0
- package/corpus/core/package.json +8 -1
- package/corpus/core/scripts/materialize-source-corpus.mjs +9 -2
- package/corpus/core/src/cli/atomic-json-file.ts +324 -0
- package/corpus/core/src/cli/claude-code-participant.ts +515 -0
- package/corpus/core/src/cli/code-agent-executor.ts +35 -15
- package/corpus/core/src/cli/code-agent-runs.concurrent-worker.ts +33 -0
- package/corpus/core/src/cli/code-agent-runs.ts +70 -36
- package/corpus/core/src/cli/codex-cli-participant.ts +496 -0
- package/corpus/core/src/cli/multi-frontier-runs.ts +1952 -0
- package/corpus/core/src/client/chat/ChatHistoryList.tsx +7 -404
- package/corpus/core/src/coding-tools/index.ts +27 -8
- package/corpus/core/src/embeddings/index.ts +233 -0
- package/corpus/core/src/index.ts +2 -0
- package/corpus/core/src/integrations/index.ts +10 -0
- package/corpus/core/src/integrations/slack-oauth.ts +1 -0
- package/corpus/core/src/integrations/webhook-delivery.ts +55 -0
- package/corpus/core/src/notifications/channels.ts +4 -10
- package/corpus/core/src/provider-api/actions/custom-provider-registration.ts +197 -0
- package/corpus/core/src/provider-api/actions/delete-staged-dataset.ts +3 -42
- package/corpus/core/src/provider-api/actions/list-staged-datasets.ts +3 -45
- package/corpus/core/src/provider-api/actions/provider-api-audit.ts +88 -0
- package/corpus/core/src/provider-api/actions/provider-api.ts +460 -0
- package/corpus/core/src/provider-api/actions/query-staged-dataset.ts +3 -140
- package/corpus/core/src/provider-api/actions/staged-datasets.ts +359 -0
- package/corpus/core/src/provider-api/gong.ts +108 -0
- package/corpus/core/src/scripts/docs/source-search.ts +2 -1
- package/corpus/core/src/search/index.ts +413 -0
- package/corpus/core/src/server/google-oauth-credentials.ts +80 -3
- package/corpus/core/src/server/index.ts +6 -0
- package/corpus/core/src/styles/chat-history-list.css +1 -313
- package/corpus/core/src/templates/default/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/core/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/corpus/core/src/templates/default/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/corpus/core/src/templates/default/.agents/skills/onboarding/SKILL.md +56 -0
- package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +273 -0
- package/corpus/core/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/core/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/core/src/templates/default/AGENTS.md +25 -18
- package/corpus/core/src/templates/headless/.agents/skills/actions/SKILL.md +512 -0
- package/corpus/core/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/corpus/core/src/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +273 -0
- package/corpus/core/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/core/src/templates/headless/AGENTS.md +7 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/core/src/templates/workspace-core/AGENTS.md +4 -0
- package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
- package/corpus/templates/analytics/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/analytics/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/analytics/actions/delete-staged-dataset.ts +3 -23
- package/corpus/templates/analytics/actions/list-staged-datasets.ts +3 -27
- package/corpus/templates/analytics/actions/provider-api-catalog.ts +16 -19
- package/corpus/templates/analytics/actions/provider-api-docs.ts +3 -5
- package/corpus/templates/analytics/actions/provider-api-request.ts +162 -185
- package/corpus/templates/analytics/actions/query-staged-dataset.ts +3 -47
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +52 -161
- package/corpus/templates/assets/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/assets/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +63 -160
- package/corpus/templates/brain/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
- package/corpus/templates/brain/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/brain/README.md +34 -3
- package/corpus/templates/brain/actions/_schemas.ts +13 -7
- package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
- package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
- package/corpus/templates/brain/actions/create-source.ts +1 -1
- package/corpus/templates/brain/actions/delete-staged-dataset.ts +3 -24
- package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
- package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
- package/corpus/templates/brain/actions/import-capture.ts +21 -9
- package/corpus/templates/brain/actions/import-transcript.ts +42 -14
- package/corpus/templates/brain/actions/list-captures.ts +10 -1
- package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
- package/corpus/templates/brain/actions/list-projects.ts +51 -0
- package/corpus/templates/brain/actions/list-proposals.ts +80 -4
- package/corpus/templates/brain/actions/list-sources.ts +17 -3
- package/corpus/templates/brain/actions/list-staged-datasets.ts +3 -28
- package/corpus/templates/brain/actions/manage-project.ts +118 -0
- package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
- package/corpus/templates/brain/actions/navigate.ts +1 -0
- package/corpus/templates/brain/actions/provider-api-catalog.ts +5 -11
- package/corpus/templates/brain/actions/provider-api-docs.ts +3 -5
- package/corpus/templates/brain/actions/provider-api-request.ts +3 -37
- package/corpus/templates/brain/actions/query-staged-dataset.ts +3 -48
- package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
- package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
- package/corpus/templates/brain/actions/review-proposal.ts +2 -0
- package/corpus/templates/brain/actions/search-everything.ts +10 -1
- package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
- package/corpus/templates/brain/actions/set-settings.ts +36 -1
- package/corpus/templates/brain/actions/update-proposal.ts +2 -0
- package/corpus/templates/brain/actions/update-source.ts +5 -1
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +48 -154
- package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/brain/app/i18n-data.ts +731 -2
- package/corpus/templates/brain/app/lib/brain.ts +88 -1
- package/corpus/templates/brain/app/lib/ingest-handoff.ts +35 -0
- package/corpus/templates/brain/app/routes/ops.tsx +47 -0
- package/corpus/templates/brain/app/routes/review.tsx +41 -2
- package/corpus/templates/brain/app/routes/search.tsx +33 -2
- package/corpus/templates/brain/app/routes/settings.tsx +158 -0
- package/corpus/templates/brain/app/routes/sources.tsx +31 -0
- package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-slack-backfills-can-now-join-configured-public-channels-befo.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-slack-backfills-now-continue-across-the-configured-number-of.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-slack-backfills-now-start-with-a-safe-four-week-window-and-r.md +6 -0
- package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/db/index.ts +18 -0
- package/corpus/templates/brain/server/db/schema.ts +197 -1
- package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
- package/corpus/templates/brain/server/lib/audiences.ts +708 -0
- package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
- package/corpus/templates/brain/server/lib/brain.ts +877 -50
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
- package/corpus/templates/brain/server/lib/connectors.ts +609 -72
- package/corpus/templates/brain/server/lib/demo.ts +17 -4
- package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
- package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
- package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
- package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
- package/corpus/templates/brain/server/lib/search-index.ts +714 -0
- package/corpus/templates/brain/server/lib/search.ts +120 -4
- package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
- package/corpus/templates/brain/server/lib/slack-channel-membership.ts +44 -0
- package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
- package/corpus/templates/brain/server/onboarding.ts +29 -0
- package/corpus/templates/brain/server/plugins/db.ts +204 -0
- package/corpus/templates/brain/server/register-secrets.ts +46 -0
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
- package/corpus/templates/brain/shared/slack-source-config.ts +60 -0
- package/corpus/templates/brain/shared/types.ts +10 -0
- package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/calendar/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/calendar/actions/delete-staged-dataset.ts +3 -24
- package/corpus/templates/calendar/actions/list-staged-datasets.ts +3 -28
- package/corpus/templates/calendar/actions/provider-api-catalog.ts +5 -11
- package/corpus/templates/calendar/actions/provider-api-docs.ts +3 -5
- package/corpus/templates/calendar/actions/provider-api-request.ts +3 -37
- package/corpus/templates/calendar/actions/query-staged-dataset.ts +3 -48
- package/corpus/templates/calendar/server/handlers/gong.ts +6 -61
- package/corpus/templates/calendar/server/handlers/google-auth.ts +10 -8
- package/corpus/templates/calendar/server/lib/google-calendar.ts +7 -45
- package/corpus/templates/chat/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/corpus/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/chat/AGENTS.md +5 -2
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +51 -157
- package/corpus/templates/clips/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/clips/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/clips/AGENTS.md +11 -0
- package/corpus/templates/clips/actions/export-to-brain.ts +505 -171
- package/corpus/templates/clips/changelog/2026-07-19-ready-clips-transcripts-can-be-backfilled-to-brain-for-a-bou.md +6 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +1 -1
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +12 -22
- package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +9 -6
- package/corpus/templates/clips/server/lib/slack-oauth.ts +12 -47
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +2 -6
- package/corpus/templates/clips/server/routes/api/auth/google-calendar.get.ts +4 -4
- package/corpus/templates/content/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/content/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/content/actions/delete-staged-dataset.ts +3 -24
- package/corpus/templates/content/actions/list-staged-datasets.ts +3 -28
- package/corpus/templates/content/actions/provider-api-catalog.ts +5 -11
- package/corpus/templates/content/actions/provider-api-docs.ts +3 -5
- package/corpus/templates/content/actions/provider-api-request.ts +3 -37
- package/corpus/templates/content/actions/query-staged-dataset.ts +3 -48
- package/corpus/templates/design/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/design/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/design/actions/delete-staged-dataset.ts +4 -23
- package/corpus/templates/design/actions/list-staged-datasets.ts +4 -27
- package/corpus/templates/design/actions/provider-api-catalog.ts +15 -18
- package/corpus/templates/design/actions/provider-api-docs.ts +57 -48
- package/corpus/templates/design/actions/provider-api-request.ts +140 -164
- package/corpus/templates/design/actions/query-staged-dataset.ts +4 -47
- package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/dispatch/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/dispatch/actions/delete-staged-dataset.ts +4 -23
- package/corpus/templates/dispatch/actions/list-staged-datasets.ts +4 -30
- package/corpus/templates/dispatch/actions/provider-api-catalog.ts +15 -18
- package/corpus/templates/dispatch/actions/provider-api-docs.ts +57 -48
- package/corpus/templates/dispatch/actions/provider-api-request.ts +138 -163
- package/corpus/templates/dispatch/actions/query-staged-dataset.ts +4 -47
- package/corpus/templates/forms/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/forms/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/forms/server/lib/integrations.ts +23 -22
- package/corpus/templates/macros/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/mail/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/mail/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/mail/actions/delete-staged-dataset.ts +4 -23
- package/corpus/templates/mail/actions/list-staged-datasets.ts +4 -27
- package/corpus/templates/mail/actions/provider-api-catalog.ts +15 -18
- package/corpus/templates/mail/actions/provider-api-docs.ts +57 -48
- package/corpus/templates/mail/actions/provider-api-request.ts +146 -173
- package/corpus/templates/mail/actions/query-staged-dataset.ts +4 -47
- package/corpus/templates/mail/server/handlers/gong.ts +6 -61
- package/corpus/templates/mail/server/lib/google-auth.ts +10 -13
- package/corpus/templates/plan/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/plan/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +47 -156
- package/corpus/templates/slides/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/slides/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/templates/slides/actions/delete-staged-dataset.ts +4 -23
- package/corpus/templates/slides/actions/list-staged-datasets.ts +4 -27
- package/corpus/templates/slides/actions/provider-api-catalog.ts +15 -18
- package/corpus/templates/slides/actions/provider-api-docs.ts +57 -48
- package/corpus/templates/slides/actions/provider-api-request.ts +145 -172
- package/corpus/templates/slides/actions/query-staged-dataset.ts +4 -47
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +11 -36
- package/corpus/templates/tasks/.agents/skills/actions/SKILL.md +32 -12
- package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/corpus/templates/tasks/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +125 -0
- package/corpus/toolkit/README.md +60 -0
- package/corpus/toolkit/export-snapshot.json +35 -0
- package/corpus/toolkit/migration-manifest.json +4 -0
- package/corpus/toolkit/package.json +281 -0
- package/corpus/toolkit/scripts/finalize-build.mjs +55 -0
- package/corpus/toolkit/src/app-shell/header-actions.tsx +84 -0
- package/corpus/toolkit/src/app-shell/index.ts +1 -0
- package/corpus/toolkit/src/chat-history/ChatHistoryList.tsx +469 -0
- package/corpus/toolkit/src/chat-history/index.ts +7 -0
- package/corpus/toolkit/src/chat-history.css +314 -0
- package/corpus/toolkit/src/collab-ui/AgentPresenceChip.tsx +69 -0
- package/corpus/toolkit/src/collab-ui/LiveCursorOverlay.tsx +241 -0
- package/corpus/toolkit/src/collab-ui/PresenceBar.tsx +346 -0
- package/corpus/toolkit/src/collab-ui/RecentEditHighlights.tsx +225 -0
- package/corpus/toolkit/src/collab-ui/RemoteSelectionRings.tsx +227 -0
- package/corpus/toolkit/src/collab-ui/agent-identity.ts +16 -0
- package/corpus/toolkit/src/collab-ui/index.ts +39 -0
- package/corpus/toolkit/src/collab-ui/lead-client.ts +32 -0
- package/corpus/toolkit/src/collab-ui/types.ts +96 -0
- package/corpus/toolkit/src/composer/AgentComposerFrame.tsx +59 -0
- package/corpus/toolkit/src/composer/ComposerPlusMenu.tsx +983 -0
- package/corpus/toolkit/src/composer/MentionPopover.tsx +412 -0
- package/corpus/toolkit/src/composer/PastedTextChip.tsx +150 -0
- package/corpus/toolkit/src/composer/PromptComposer.tsx +718 -0
- package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +856 -0
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +2936 -0
- package/corpus/toolkit/src/composer/VoiceButton.tsx +255 -0
- package/corpus/toolkit/src/composer/asset-picker-url.ts +80 -0
- package/corpus/toolkit/src/composer/attachment-accept.ts +71 -0
- package/corpus/toolkit/src/composer/draft-key.ts +7 -0
- package/corpus/toolkit/src/composer/extensions/FileReference.tsx +55 -0
- package/corpus/toolkit/src/composer/extensions/MentionReference.tsx +93 -0
- package/corpus/toolkit/src/composer/extensions/SkillReference.tsx +56 -0
- package/corpus/toolkit/src/composer/index.ts +79 -0
- package/corpus/toolkit/src/composer/pasted-text.ts +160 -0
- package/corpus/toolkit/src/composer/prompt-attachments.ts +93 -0
- package/corpus/toolkit/src/composer/realtime-voice-audio-level.ts +64 -0
- package/corpus/toolkit/src/composer/realtime-voice-transcript.ts +134 -0
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +343 -0
- package/corpus/toolkit/src/composer/types.ts +66 -0
- package/corpus/toolkit/src/composer/use-file-search.ts +64 -0
- package/corpus/toolkit/src/composer/use-mention-search.ts +90 -0
- package/corpus/toolkit/src/composer/use-skills.ts +45 -0
- package/corpus/toolkit/src/composer/useRealtimeVoiceMode.tsx +2117 -0
- package/corpus/toolkit/src/composer/useVoiceDictation.ts +1191 -0
- package/corpus/toolkit/src/context-ui/ContextMeter.tsx +101 -0
- package/corpus/toolkit/src/context-ui/ContextSegmentRow.tsx +184 -0
- package/corpus/toolkit/src/context-ui/ContextTreemap.tsx +125 -0
- package/corpus/toolkit/src/context-ui/ContextXRayPanel.tsx +377 -0
- package/corpus/toolkit/src/context-ui/format.ts +42 -0
- package/corpus/toolkit/src/context-ui/index.ts +15 -0
- package/corpus/toolkit/src/context-ui/types.ts +43 -0
- package/corpus/toolkit/src/design-tweaks/index.ts +1 -0
- package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +1084 -0
- package/corpus/toolkit/src/editor/BubbleToolbar.tsx +253 -0
- package/corpus/toolkit/src/editor/CodeBlockNode.tsx +254 -0
- package/corpus/toolkit/src/editor/DragHandle.ts +1437 -0
- package/corpus/toolkit/src/editor/ImageExtension.ts +306 -0
- package/corpus/toolkit/src/editor/RegistryBlockContext.tsx +107 -0
- package/corpus/toolkit/src/editor/RegistryBlockNode.tsx +652 -0
- package/corpus/toolkit/src/editor/RichMarkdownEditor.tsx +81 -0
- package/corpus/toolkit/src/editor/RunId.ts +61 -0
- package/corpus/toolkit/src/editor/SharedRichEditor.tsx +321 -0
- package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +310 -0
- package/corpus/toolkit/src/editor/extensions.ts +402 -0
- package/corpus/toolkit/src/editor/gfmDoc.ts +92 -0
- package/corpus/toolkit/src/editor/index.ts +93 -0
- package/corpus/toolkit/src/editor/registrySlashCommands.ts +131 -0
- package/corpus/toolkit/src/editor/surgical-apply.ts +192 -0
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +589 -0
- package/corpus/toolkit/src/editor.css +510 -0
- package/corpus/toolkit/src/hooks/index.ts +2 -0
- package/corpus/toolkit/src/hooks/use-mobile.tsx +21 -0
- package/corpus/toolkit/src/hooks/use-toast.ts +188 -0
- package/corpus/toolkit/src/index.ts +8 -0
- package/corpus/toolkit/src/onboarding/index.ts +1 -0
- package/corpus/toolkit/src/provider.tsx +47 -0
- package/corpus/toolkit/src/sharing/VisibilityBadge.tsx +37 -0
- package/corpus/toolkit/src/sharing/index.ts +4 -0
- package/corpus/toolkit/src/styles.css +21 -0
- package/corpus/toolkit/src/ui/accordion.tsx +56 -0
- package/corpus/toolkit/src/ui/alert-dialog.tsx +141 -0
- package/corpus/toolkit/src/ui/alert.tsx +59 -0
- package/corpus/toolkit/src/ui/aspect-ratio.tsx +5 -0
- package/corpus/toolkit/src/ui/avatar.tsx +48 -0
- package/corpus/toolkit/src/ui/badge.tsx +37 -0
- package/corpus/toolkit/src/ui/breadcrumb.tsx +115 -0
- package/corpus/toolkit/src/ui/button.tsx +78 -0
- package/corpus/toolkit/src/ui/calendar.tsx +214 -0
- package/corpus/toolkit/src/ui/card.tsx +86 -0
- package/corpus/toolkit/src/ui/carousel.tsx +260 -0
- package/corpus/toolkit/src/ui/chart.tsx +375 -0
- package/corpus/toolkit/src/ui/checkbox.tsx +28 -0
- package/corpus/toolkit/src/ui/collapsible.tsx +9 -0
- package/corpus/toolkit/src/ui/command.tsx +174 -0
- package/corpus/toolkit/src/ui/context-menu.tsx +202 -0
- package/corpus/toolkit/src/ui/date-picker.tsx +64 -0
- package/corpus/toolkit/src/ui/dialog.tsx +146 -0
- package/corpus/toolkit/src/ui/drawer.tsx +126 -0
- package/corpus/toolkit/src/ui/dropdown-menu.tsx +218 -0
- package/corpus/toolkit/src/ui/form.tsx +177 -0
- package/corpus/toolkit/src/ui/hover-card.tsx +27 -0
- package/corpus/toolkit/src/ui/index.ts +52 -0
- package/corpus/toolkit/src/ui/input-otp.tsx +69 -0
- package/corpus/toolkit/src/ui/input.tsx +22 -0
- package/corpus/toolkit/src/ui/label.tsx +24 -0
- package/corpus/toolkit/src/ui/logo.tsx +36 -0
- package/corpus/toolkit/src/ui/menubar.tsx +235 -0
- package/corpus/toolkit/src/ui/navigation-menu.tsx +128 -0
- package/corpus/toolkit/src/ui/pagination.tsx +121 -0
- package/corpus/toolkit/src/ui/popover.tsx +44 -0
- package/corpus/toolkit/src/ui/progress.tsx +26 -0
- package/corpus/toolkit/src/ui/radio-group.tsx +42 -0
- package/corpus/toolkit/src/ui/resizable.tsx +43 -0
- package/corpus/toolkit/src/ui/scroll-area.tsx +46 -0
- package/corpus/toolkit/src/ui/select.tsx +158 -0
- package/corpus/toolkit/src/ui/separator.tsx +29 -0
- package/corpus/toolkit/src/ui/sheet.tsx +160 -0
- package/corpus/toolkit/src/ui/sidebar.tsx +777 -0
- package/corpus/toolkit/src/ui/skeleton.tsx +18 -0
- package/corpus/toolkit/src/ui/slider.tsx +26 -0
- package/corpus/toolkit/src/ui/sonner.tsx +58 -0
- package/corpus/toolkit/src/ui/spinner.tsx +17 -0
- package/corpus/toolkit/src/ui/switch.tsx +27 -0
- package/corpus/toolkit/src/ui/table.tsx +117 -0
- package/corpus/toolkit/src/ui/tabs.tsx +53 -0
- package/corpus/toolkit/src/ui/textarea.tsx +23 -0
- package/corpus/toolkit/src/ui/toast.tsx +127 -0
- package/corpus/toolkit/src/ui/toaster.tsx +33 -0
- package/corpus/toolkit/src/ui/toggle-group.tsx +59 -0
- package/corpus/toolkit/src/ui/toggle.tsx +43 -0
- package/corpus/toolkit/src/ui/tooltip.tsx +53 -0
- package/corpus/toolkit/src/ui/use-toast.ts +3 -0
- package/corpus/toolkit/src/utils.ts +8 -0
- package/corpus/toolkit/tsconfig.json +23 -0
- package/dist/cli/atomic-json-file.d.ts +30 -0
- package/dist/cli/atomic-json-file.d.ts.map +1 -0
- package/dist/cli/atomic-json-file.js +240 -0
- package/dist/cli/atomic-json-file.js.map +1 -0
- package/dist/cli/claude-code-participant.d.ts +77 -0
- package/dist/cli/claude-code-participant.d.ts.map +1 -0
- package/dist/cli/claude-code-participant.js +355 -0
- package/dist/cli/claude-code-participant.js.map +1 -0
- package/dist/cli/code-agent-executor.d.ts +8 -9
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +27 -13
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/code-agent-runs.concurrent-worker.d.ts +2 -0
- package/dist/cli/code-agent-runs.concurrent-worker.d.ts.map +1 -0
- package/dist/cli/code-agent-runs.concurrent-worker.js +32 -0
- package/dist/cli/code-agent-runs.concurrent-worker.js.map +1 -0
- package/dist/cli/code-agent-runs.d.ts +5 -0
- package/dist/cli/code-agent-runs.d.ts.map +1 -1
- package/dist/cli/code-agent-runs.js +51 -33
- package/dist/cli/code-agent-runs.js.map +1 -1
- package/dist/cli/codex-cli-participant.d.ts +77 -0
- package/dist/cli/codex-cli-participant.d.ts.map +1 -0
- package/dist/cli/codex-cli-participant.js +354 -0
- package/dist/cli/codex-cli-participant.js.map +1 -0
- package/dist/cli/multi-frontier-runs.d.ts +246 -0
- package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
- package/dist/cli/multi-frontier-runs.js +1247 -0
- package/dist/cli/multi-frontier-runs.js.map +1 -0
- package/dist/client/chat/ChatHistoryList.d.ts +1 -87
- package/dist/client/chat/ChatHistoryList.d.ts.map +1 -1
- package/dist/client/chat/ChatHistoryList.js +1 -82
- package/dist/client/chat/ChatHistoryList.js.map +1 -1
- package/dist/coding-tools/index.d.ts +1 -0
- package/dist/coding-tools/index.d.ts.map +1 -1
- package/dist/coding-tools/index.js +25 -7
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/embeddings/index.d.ts +24 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +172 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +2 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/slack-manifest.d.ts +1 -1
- package/dist/integrations/slack-oauth.d.ts +1 -1
- package/dist/integrations/slack-oauth.d.ts.map +1 -1
- package/dist/integrations/slack-oauth.js +1 -0
- package/dist/integrations/slack-oauth.js.map +1 -1
- package/dist/integrations/webhook-delivery.d.ts +23 -0
- package/dist/integrations/webhook-delivery.d.ts.map +1 -0
- package/dist/integrations/webhook-delivery.js +32 -0
- package/dist/integrations/webhook-delivery.js.map +1 -0
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +4 -9
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +136 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts.map +1 -0
- package/dist/provider-api/actions/custom-provider-registration.js +154 -0
- package/dist/provider-api/actions/custom-provider-registration.js.map +1 -0
- package/dist/provider-api/actions/delete-staged-dataset.d.ts +1 -1
- package/dist/provider-api/actions/delete-staged-dataset.d.ts.map +1 -1
- package/dist/provider-api/actions/delete-staged-dataset.js +3 -34
- package/dist/provider-api/actions/delete-staged-dataset.js.map +1 -1
- package/dist/provider-api/actions/list-staged-datasets.d.ts +2 -3
- package/dist/provider-api/actions/list-staged-datasets.d.ts.map +1 -1
- package/dist/provider-api/actions/list-staged-datasets.js +3 -40
- package/dist/provider-api/actions/list-staged-datasets.js.map +1 -1
- package/dist/provider-api/actions/provider-api-audit.d.ts +7 -0
- package/dist/provider-api/actions/provider-api-audit.d.ts.map +1 -0
- package/dist/provider-api/actions/provider-api-audit.js +74 -0
- package/dist/provider-api/actions/provider-api-audit.js.map +1 -0
- package/dist/provider-api/actions/provider-api.d.ts +370 -0
- package/dist/provider-api/actions/provider-api.d.ts.map +1 -0
- package/dist/provider-api/actions/provider-api.js +280 -0
- package/dist/provider-api/actions/provider-api.js.map +1 -0
- package/dist/provider-api/actions/query-staged-dataset.d.ts +1 -1
- package/dist/provider-api/actions/query-staged-dataset.d.ts.map +1 -1
- package/dist/provider-api/actions/query-staged-dataset.js +3 -116
- package/dist/provider-api/actions/query-staged-dataset.js.map +1 -1
- package/dist/provider-api/actions/staged-datasets.d.ts +190 -0
- package/dist/provider-api/actions/staged-datasets.d.ts.map +1 -0
- package/dist/provider-api/actions/staged-datasets.js +210 -0
- package/dist/provider-api/actions/staged-datasets.js.map +1 -0
- package/dist/provider-api/gong.d.ts +44 -0
- package/dist/provider-api/gong.d.ts.map +1 -0
- package/dist/provider-api/gong.js +56 -0
- package/dist/provider-api/gong.js.map +1 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/docs/source-search.d.ts +2 -1
- package/dist/scripts/docs/source-search.d.ts.map +1 -1
- package/dist/scripts/docs/source-search.js +2 -1
- package/dist/scripts/docs/source-search.js.map +1 -1
- package/dist/search/index.d.ts +88 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +267 -0
- package/dist/search/index.js.map +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/google-oauth-credentials.d.ts +35 -0
- package/dist/server/google-oauth-credentials.d.ts.map +1 -1
- package/dist/server/google-oauth-credentials.js +48 -2
- package/dist/server/google-oauth-credentials.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/styles/chat-history-list.css +1 -313
- package/dist/templates/chat/.agents/skills/actions/SKILL.md +32 -12
- package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/dist/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/dist/templates/chat/AGENTS.md +5 -2
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +51 -157
- package/dist/templates/default/.agents/skills/actions/SKILL.md +32 -12
- package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/dist/templates/default/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/dist/templates/default/.agents/skills/onboarding/SKILL.md +56 -0
- package/dist/templates/default/.agents/skills/secrets/SKILL.md +273 -0
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/dist/templates/default/AGENTS.md +25 -18
- package/dist/templates/headless/.agents/skills/actions/SKILL.md +512 -0
- package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/dist/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/dist/templates/headless/.agents/skills/secrets/SKILL.md +273 -0
- package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/dist/templates/headless/AGENTS.md +7 -0
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -12
- package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +6 -0
- package/dist/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/dist/templates/workspace-core/AGENTS.md +4 -0
- package/dist/templates/workspace-root/AGENTS.md +4 -0
- package/docs/AGENTS.md +7 -0
- package/docs/content/integrations.mdx +63 -2
- package/docs/design/provider-integration-authority.md +117 -0
- package/package.json +9 -2
- package/src/templates/chat/.agents/skills/actions/SKILL.md +32 -12
- package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/src/templates/chat/AGENTS.md +5 -2
- package/src/templates/chat/app/components/layout/Sidebar.tsx +51 -157
- package/src/templates/default/.agents/skills/actions/SKILL.md +32 -12
- package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/src/templates/default/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/src/templates/default/.agents/skills/onboarding/SKILL.md +56 -0
- package/src/templates/default/.agents/skills/secrets/SKILL.md +273 -0
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/src/templates/default/AGENTS.md +25 -18
- package/src/templates/headless/.agents/skills/actions/SKILL.md +512 -0
- package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/src/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +323 -0
- package/src/templates/headless/.agents/skills/secrets/SKILL.md +273 -0
- package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/src/templates/headless/AGENTS.md +7 -0
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -12
- package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +10 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +6 -0
- package/src/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +102 -0
- package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +11 -2
- package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +6 -0
- package/src/templates/workspace-core/AGENTS.md +4 -0
- package/src/templates/workspace-root/AGENTS.md +4 -0
|
@@ -0,0 +1,1952 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
appendUniqueJsonLineAtomically,
|
|
6
|
+
withFileLockSync,
|
|
7
|
+
writeJsonFileAtomically,
|
|
8
|
+
writeTextFileAtomically,
|
|
9
|
+
} from "./atomic-json-file.js";
|
|
10
|
+
import { codeAgentStoreRoot } from "./code-agent-runs.js";
|
|
11
|
+
|
|
12
|
+
export type MultiFrontierPhase =
|
|
13
|
+
| "proposing"
|
|
14
|
+
| "cross_review"
|
|
15
|
+
| "converging"
|
|
16
|
+
| "awaiting_go"
|
|
17
|
+
| "implementing"
|
|
18
|
+
| "checkpoint_review"
|
|
19
|
+
| "paused"
|
|
20
|
+
| "completed"
|
|
21
|
+
| "failed"
|
|
22
|
+
| "canceled";
|
|
23
|
+
|
|
24
|
+
export type MultiFrontierParticipantRole = "driver" | "watchdog";
|
|
25
|
+
export type MultiFrontierParticipantPermission =
|
|
26
|
+
| "read_only"
|
|
27
|
+
| "workspace_write";
|
|
28
|
+
export type MultiFrontierParticipantStatus =
|
|
29
|
+
| "idle"
|
|
30
|
+
| "running"
|
|
31
|
+
| "waiting"
|
|
32
|
+
| "failed"
|
|
33
|
+
| "completed";
|
|
34
|
+
export type MultiFrontierApprovalState =
|
|
35
|
+
| "not_required"
|
|
36
|
+
| "pending"
|
|
37
|
+
| "approved"
|
|
38
|
+
| "rejected";
|
|
39
|
+
export type MultiFrontierRecoveryReason =
|
|
40
|
+
| "main_process_restarted"
|
|
41
|
+
| "driver_crashed"
|
|
42
|
+
| "watchdog_crashed"
|
|
43
|
+
| "app_quit"
|
|
44
|
+
| "canceled";
|
|
45
|
+
|
|
46
|
+
export interface MultiFrontierParticipantState {
|
|
47
|
+
participantId: string;
|
|
48
|
+
provider: string;
|
|
49
|
+
runtime: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
capabilities?: string[];
|
|
52
|
+
sessionRef?: string;
|
|
53
|
+
role: MultiFrontierParticipantRole;
|
|
54
|
+
permission: MultiFrontierParticipantPermission;
|
|
55
|
+
status: MultiFrontierParticipantStatus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MultiFrontierDriverLease {
|
|
59
|
+
participantId: string;
|
|
60
|
+
generation: number;
|
|
61
|
+
leaseState: "inactive" | "active" | "revoked";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MultiFrontierApproval {
|
|
65
|
+
state: MultiFrontierApprovalState;
|
|
66
|
+
proposalId?: string;
|
|
67
|
+
reviewPacketId?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MultiFrontierRecovery {
|
|
71
|
+
reason: MultiFrontierRecoveryReason;
|
|
72
|
+
recoveredAt: string;
|
|
73
|
+
resumablePhase: MultiFrontierPhase;
|
|
74
|
+
checkpointId?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The durable state contract shared by the main process and every participant.
|
|
79
|
+
* Only the main-process coordinator writes it through this module.
|
|
80
|
+
*/
|
|
81
|
+
export interface MultiFrontierRunState {
|
|
82
|
+
schemaVersion: 1;
|
|
83
|
+
collaborationId: string;
|
|
84
|
+
/** Opaque local workspace handle; never a filesystem path. */
|
|
85
|
+
workspaceId?: string;
|
|
86
|
+
phase: MultiFrontierPhase;
|
|
87
|
+
participants: MultiFrontierParticipantState[];
|
|
88
|
+
driver: MultiFrontierDriverLease | null;
|
|
89
|
+
approval: MultiFrontierApproval;
|
|
90
|
+
checkpointIds: string[];
|
|
91
|
+
round: number;
|
|
92
|
+
proposalIds: string[];
|
|
93
|
+
reviewIds: string[];
|
|
94
|
+
/** Opt-in policy; explicit GO remains the durable default. */
|
|
95
|
+
autoContinueAfterAgreement: boolean;
|
|
96
|
+
recovery?: MultiFrontierRecovery;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CreateMultiFrontierRunInput {
|
|
100
|
+
collaborationId: string;
|
|
101
|
+
workspaceId?: string;
|
|
102
|
+
phase?: MultiFrontierPhase;
|
|
103
|
+
participants: MultiFrontierParticipantState[];
|
|
104
|
+
approval?: MultiFrontierApproval;
|
|
105
|
+
checkpointIds?: string[];
|
|
106
|
+
autoContinueAfterAgreement?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface RecoverMultiFrontierRunInput {
|
|
110
|
+
now: string;
|
|
111
|
+
reason: MultiFrontierRecoveryReason;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface MultiFrontierParticipantEvent {
|
|
115
|
+
participantId: string;
|
|
116
|
+
generation?: number;
|
|
117
|
+
permission: MultiFrontierParticipantPermission;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PersistedMultiFrontierParticipantEvent extends MultiFrontierParticipantEvent {
|
|
121
|
+
schemaVersion: 1;
|
|
122
|
+
id: string;
|
|
123
|
+
collaborationId: string;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
status?: MultiFrontierParticipantStatus;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type MultiFrontierArtifactKind = "proposal" | "review" | "checkpoint";
|
|
129
|
+
|
|
130
|
+
export interface MultiFrontierArtifactTestSummary {
|
|
131
|
+
name: string;
|
|
132
|
+
status: "passed" | "failed" | "skipped";
|
|
133
|
+
summary?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A bounded, redacted projection used only for Desktop orchestrator recovery. */
|
|
137
|
+
export interface MultiFrontierOrchestrationArtifact {
|
|
138
|
+
id: string;
|
|
139
|
+
kind:
|
|
140
|
+
| "proposal"
|
|
141
|
+
| "cross_review"
|
|
142
|
+
| "revision"
|
|
143
|
+
| "synthesis"
|
|
144
|
+
| "checkpoint"
|
|
145
|
+
| "watchdog_review"
|
|
146
|
+
| "finding_disposition"
|
|
147
|
+
| "completion";
|
|
148
|
+
round: number;
|
|
149
|
+
participantId?: string;
|
|
150
|
+
text: string;
|
|
151
|
+
attribution: { participantIds: string[]; sourceArtifactIds: string[] };
|
|
152
|
+
supersedesArtifactId?: string;
|
|
153
|
+
metadata?: Record<string, unknown>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A deliberately narrow coordination record. Full diffs, transcripts, and
|
|
158
|
+
* provider payloads stay in their owning runtime rather than this durable
|
|
159
|
+
* cross-provider index.
|
|
160
|
+
*/
|
|
161
|
+
export interface PersistedMultiFrontierArtifact {
|
|
162
|
+
schemaVersion: 1;
|
|
163
|
+
id: string;
|
|
164
|
+
collaborationId: string;
|
|
165
|
+
kind: MultiFrontierArtifactKind;
|
|
166
|
+
createdAt: string;
|
|
167
|
+
participantId?: string;
|
|
168
|
+
title: string;
|
|
169
|
+
summary: string;
|
|
170
|
+
supersedesArtifactId?: string;
|
|
171
|
+
contentHash?: string;
|
|
172
|
+
fileRefs?: string[];
|
|
173
|
+
testSummary?: MultiFrontierArtifactTestSummary[];
|
|
174
|
+
orchestration?: MultiFrontierOrchestrationArtifact;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface AppendMultiFrontierArtifactInput {
|
|
178
|
+
id: string;
|
|
179
|
+
collaborationId: string;
|
|
180
|
+
kind: MultiFrontierArtifactKind;
|
|
181
|
+
createdAt?: string;
|
|
182
|
+
participantId?: string;
|
|
183
|
+
title: string;
|
|
184
|
+
summary: string;
|
|
185
|
+
supersedesArtifactId?: string;
|
|
186
|
+
contentHash?: string;
|
|
187
|
+
fileRefs?: string[];
|
|
188
|
+
testSummary?: MultiFrontierArtifactTestSummary[];
|
|
189
|
+
orchestration?: MultiFrontierOrchestrationArtifact;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface MultiFrontierParticipantEventRetention {
|
|
193
|
+
schemaVersion: 1;
|
|
194
|
+
retainedEventCount: number;
|
|
195
|
+
retainedByteCount: number;
|
|
196
|
+
droppedEventCount: number;
|
|
197
|
+
droppedByteCount: number;
|
|
198
|
+
truncated: boolean;
|
|
199
|
+
replay: {
|
|
200
|
+
requiresSnapshot: boolean;
|
|
201
|
+
firstRetainedEventId?: string;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface MultiFrontierParticipantEventRetentionLimits {
|
|
206
|
+
maxEventCount: number;
|
|
207
|
+
maxBytes: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface AppendMultiFrontierParticipantEventInput extends MultiFrontierParticipantEvent {
|
|
211
|
+
id: string;
|
|
212
|
+
collaborationId: string;
|
|
213
|
+
createdAt?: string;
|
|
214
|
+
status?: MultiFrontierParticipantStatus;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface MultiFrontierStoredRun extends MultiFrontierRunState {
|
|
218
|
+
createdAt: string;
|
|
219
|
+
updatedAt: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The desktop main-process coordinator is the sole caller of this transition
|
|
224
|
+
* boundary. Its callback receives a detached snapshot, never renderer input.
|
|
225
|
+
*/
|
|
226
|
+
export type MultiFrontierCoordinatorTransition = (
|
|
227
|
+
current: MultiFrontierStoredRun,
|
|
228
|
+
) => MultiFrontierRunState | MultiFrontierStoredRun | null;
|
|
229
|
+
|
|
230
|
+
export type AppendMultiFrontierParticipantEventResult =
|
|
231
|
+
| {
|
|
232
|
+
accepted: true;
|
|
233
|
+
deduplicated: boolean;
|
|
234
|
+
event: PersistedMultiFrontierParticipantEvent;
|
|
235
|
+
run: MultiFrontierStoredRun;
|
|
236
|
+
}
|
|
237
|
+
| {
|
|
238
|
+
accepted: false;
|
|
239
|
+
reason:
|
|
240
|
+
| "missing-run"
|
|
241
|
+
| "stale-driver"
|
|
242
|
+
| "missing-participant"
|
|
243
|
+
| "event-conflict"
|
|
244
|
+
| "terminal-participant";
|
|
245
|
+
run: MultiFrontierStoredRun | null;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export type AppendMultiFrontierArtifactResult =
|
|
249
|
+
| {
|
|
250
|
+
accepted: true;
|
|
251
|
+
deduplicated: boolean;
|
|
252
|
+
artifact: PersistedMultiFrontierArtifact;
|
|
253
|
+
run: MultiFrontierStoredRun;
|
|
254
|
+
}
|
|
255
|
+
| {
|
|
256
|
+
accepted: false;
|
|
257
|
+
reason:
|
|
258
|
+
| "missing-run"
|
|
259
|
+
| "artifact-conflict"
|
|
260
|
+
| "missing-participant"
|
|
261
|
+
| "missing-superseded-artifact"
|
|
262
|
+
| "artifact-limit-reached";
|
|
263
|
+
run: MultiFrontierStoredRun | null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const TERMINAL_PHASES = new Set<MultiFrontierPhase>([
|
|
267
|
+
"completed",
|
|
268
|
+
"failed",
|
|
269
|
+
"canceled",
|
|
270
|
+
]);
|
|
271
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,191}$/;
|
|
272
|
+
const SAFE_CONTENT_HASH = /^[a-f0-9]{64}$/;
|
|
273
|
+
const SAFE_FILE_REF = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/@+-]+$/;
|
|
274
|
+
const MAX_ARTIFACT_TITLE_BYTES = 280;
|
|
275
|
+
const MAX_ARTIFACT_SUMMARY_BYTES = 8 * 1024;
|
|
276
|
+
const MAX_ARTIFACT_FILE_REFS = 40;
|
|
277
|
+
const MAX_ARTIFACT_TEST_SUMMARIES = 40;
|
|
278
|
+
const MAX_ARTIFACT_TEST_NAME_BYTES = 280;
|
|
279
|
+
const MAX_ARTIFACT_TEST_SUMMARY_BYTES = 2 * 1024;
|
|
280
|
+
export const MAX_MULTI_FRONTIER_ARTIFACTS_PER_RUN = 200;
|
|
281
|
+
export const DEFAULT_MULTI_FRONTIER_PARTICIPANT_EVENT_RETENTION: MultiFrontierParticipantEventRetentionLimits =
|
|
282
|
+
{
|
|
283
|
+
maxEventCount: 2_000,
|
|
284
|
+
maxBytes: 1_000_000,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** All multi-frontier state stays alongside the existing local Code store. */
|
|
288
|
+
export function multiFrontierRunsStoreRoot(): string {
|
|
289
|
+
return path.join(codeAgentStoreRoot(), "multi-frontier");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function multiFrontierRunsDir(): string {
|
|
293
|
+
return path.join(multiFrontierRunsStoreRoot(), "runs");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function multiFrontierParticipantEventsDir(): string {
|
|
297
|
+
return path.join(multiFrontierRunsStoreRoot(), "events");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function multiFrontierArtifactsDir(): string {
|
|
301
|
+
return path.join(multiFrontierRunsStoreRoot(), "artifacts");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function createMultiFrontierRun(
|
|
305
|
+
input: CreateMultiFrontierRunInput,
|
|
306
|
+
): MultiFrontierStoredRun {
|
|
307
|
+
assertSafeId(input.collaborationId, "collaboration id");
|
|
308
|
+
if (input.workspaceId !== undefined) {
|
|
309
|
+
assertSafeId(input.workspaceId, "workspace id");
|
|
310
|
+
}
|
|
311
|
+
assertParticipants(input.participants);
|
|
312
|
+
if (getMultiFrontierRun(input.collaborationId)) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Multi-frontier run already exists: ${input.collaborationId}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const now = new Date().toISOString();
|
|
318
|
+
const state: MultiFrontierStoredRun = {
|
|
319
|
+
schemaVersion: 1,
|
|
320
|
+
collaborationId: input.collaborationId,
|
|
321
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
322
|
+
phase: input.phase ?? "proposing",
|
|
323
|
+
participants: input.participants.map((participant) => ({
|
|
324
|
+
...participant,
|
|
325
|
+
capabilities: [...(participant.capabilities ?? [])],
|
|
326
|
+
role: "watchdog",
|
|
327
|
+
permission: "read_only",
|
|
328
|
+
})),
|
|
329
|
+
driver: null,
|
|
330
|
+
approval: { ...(input.approval ?? { state: "not_required" }) },
|
|
331
|
+
checkpointIds: [...(input.checkpointIds ?? [])],
|
|
332
|
+
round: 1,
|
|
333
|
+
proposalIds: [],
|
|
334
|
+
reviewIds: [],
|
|
335
|
+
autoContinueAfterAgreement: input.autoContinueAfterAgreement ?? false,
|
|
336
|
+
createdAt: now,
|
|
337
|
+
updatedAt: now,
|
|
338
|
+
};
|
|
339
|
+
writeNewMultiFrontierRun(state);
|
|
340
|
+
return state;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function getMultiFrontierRun(
|
|
344
|
+
collaborationId: string,
|
|
345
|
+
): MultiFrontierStoredRun | null {
|
|
346
|
+
if (!SAFE_ID.test(collaborationId)) return null;
|
|
347
|
+
return readStoredRun(multiFrontierRunPath(collaborationId));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function listMultiFrontierRuns(): MultiFrontierStoredRun[] {
|
|
351
|
+
const directory = multiFrontierRunsDir();
|
|
352
|
+
if (!fs.existsSync(directory)) return [];
|
|
353
|
+
return fs
|
|
354
|
+
.readdirSync(directory)
|
|
355
|
+
.filter((file) => file.endsWith(".json"))
|
|
356
|
+
.map((file) => readStoredRun(path.join(directory, file)))
|
|
357
|
+
.filter((run): run is MultiFrontierStoredRun => run !== null)
|
|
358
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Recovery never resumes a native participant automatically. Any in-flight
|
|
363
|
+
* driver lease is revoked and requires an explicit, newly fenced activation.
|
|
364
|
+
*/
|
|
365
|
+
export function recoverMultiFrontierRun(
|
|
366
|
+
state: MultiFrontierRunState,
|
|
367
|
+
input: RecoverMultiFrontierRunInput,
|
|
368
|
+
): MultiFrontierRunState {
|
|
369
|
+
assertTimestamp(input.now, "recovery time");
|
|
370
|
+
if (TERMINAL_PHASES.has(state.phase) || state.phase === "paused") {
|
|
371
|
+
return state;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const checkpointId = state.checkpointIds.at(-1);
|
|
375
|
+
return {
|
|
376
|
+
...state,
|
|
377
|
+
phase: "paused",
|
|
378
|
+
participants: state.participants.map((participant) => ({
|
|
379
|
+
...participant,
|
|
380
|
+
role: "watchdog",
|
|
381
|
+
permission: "read_only",
|
|
382
|
+
status: participant.status === "running" ? "waiting" : participant.status,
|
|
383
|
+
})),
|
|
384
|
+
driver: state.driver ? { ...state.driver, leaseState: "revoked" } : null,
|
|
385
|
+
recovery: {
|
|
386
|
+
reason: input.reason,
|
|
387
|
+
recoveredAt: input.now,
|
|
388
|
+
resumablePhase: state.phase,
|
|
389
|
+
...(checkpointId ? { checkpointId } : {}),
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Persist the recovery decision made by the main-process coordinator. */
|
|
395
|
+
export function recoverStoredMultiFrontierRun(
|
|
396
|
+
collaborationId: string,
|
|
397
|
+
input: RecoverMultiFrontierRunInput,
|
|
398
|
+
): MultiFrontierStoredRun | null {
|
|
399
|
+
return mutateStoredMultiFrontierRun(collaborationId, input.now, (current) => {
|
|
400
|
+
const recovered = recoverMultiFrontierRun(current, input);
|
|
401
|
+
return recovered === current ? current : recovered;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Applies one durable coordinator-owned state transition under the run lock.
|
|
407
|
+
* A state-shaped result retains proposal and review references by default;
|
|
408
|
+
* returning a full stored record is the explicit escape hatch for a
|
|
409
|
+
* coordinator operation that intentionally changes those references.
|
|
410
|
+
*/
|
|
411
|
+
export function transitionStoredMultiFrontierRun(
|
|
412
|
+
collaborationId: string,
|
|
413
|
+
updatedAt: string,
|
|
414
|
+
transition: MultiFrontierCoordinatorTransition,
|
|
415
|
+
): MultiFrontierStoredRun | null {
|
|
416
|
+
assertSafeId(collaborationId, "collaboration id");
|
|
417
|
+
assertTimestamp(updatedAt, "transition time");
|
|
418
|
+
if (typeof transition !== "function") {
|
|
419
|
+
throw new Error("Invalid multi-frontier coordinator transition.");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return mutateStoredMultiFrontierRun(collaborationId, updatedAt, (current) => {
|
|
423
|
+
const candidate = transition(cloneStoredMultiFrontierRun(current));
|
|
424
|
+
if (candidate === null) return null;
|
|
425
|
+
if (isThenable(candidate)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Multi-frontier coordinator transitions must be synchronous.",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (
|
|
431
|
+
!candidate ||
|
|
432
|
+
typeof candidate !== "object" ||
|
|
433
|
+
Array.isArray(candidate)
|
|
434
|
+
) {
|
|
435
|
+
throw new Error("Invalid multi-frontier coordinator transition state.");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const isFullStoredRun = hasStoredRunTimestamps(candidate);
|
|
439
|
+
if (
|
|
440
|
+
!isFullStoredRun &&
|
|
441
|
+
(Object.hasOwn(candidate, "createdAt") ||
|
|
442
|
+
Object.hasOwn(candidate, "updatedAt"))
|
|
443
|
+
) {
|
|
444
|
+
throw new Error("Invalid multi-frontier coordinator transition state.");
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
candidate.schemaVersion !== current.schemaVersion ||
|
|
448
|
+
candidate.collaborationId !== current.collaborationId
|
|
449
|
+
) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
"Multi-frontier coordinator transitions cannot change run identity.",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (isFullStoredRun) {
|
|
455
|
+
if (!isCompleteMultiFrontierStoredRun(candidate)) {
|
|
456
|
+
throw new Error("Invalid multi-frontier coordinator transition state.");
|
|
457
|
+
}
|
|
458
|
+
if (candidate.createdAt !== current.createdAt) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"Multi-frontier coordinator transitions cannot change creation time.",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const nextState: MultiFrontierRunState = {
|
|
466
|
+
...candidate,
|
|
467
|
+
...(isFullStoredRun
|
|
468
|
+
? {}
|
|
469
|
+
: {
|
|
470
|
+
proposalIds: [...current.proposalIds],
|
|
471
|
+
reviewIds: [...current.reviewIds],
|
|
472
|
+
}),
|
|
473
|
+
};
|
|
474
|
+
if (!isCompleteMultiFrontierRunState(nextState)) {
|
|
475
|
+
throw new Error("Invalid multi-frontier coordinator transition state.");
|
|
476
|
+
}
|
|
477
|
+
return nextState;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Coordinators must call this before applying a participant event. Write
|
|
483
|
+
* events are accepted only from the active driver generation.
|
|
484
|
+
*/
|
|
485
|
+
export function canApplyMultiFrontierParticipantEvent(
|
|
486
|
+
state: MultiFrontierRunState,
|
|
487
|
+
event: MultiFrontierParticipantEvent,
|
|
488
|
+
): boolean {
|
|
489
|
+
if (TERMINAL_PHASES.has(state.phase) || state.phase === "paused") {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (
|
|
493
|
+
!state.participants.some(
|
|
494
|
+
(candidate) => candidate.participantId === event.participantId,
|
|
495
|
+
)
|
|
496
|
+
) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if (event.permission === "read_only") return true;
|
|
500
|
+
const driver = state.driver;
|
|
501
|
+
return Boolean(
|
|
502
|
+
state.phase === "implementing" &&
|
|
503
|
+
driver?.leaseState === "active" &&
|
|
504
|
+
driver.participantId === event.participantId &&
|
|
505
|
+
driver.generation === event.generation,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Explicitly restores a write lease after recovery. The generation increments
|
|
511
|
+
* so an event from the interrupted process can never regain write authority.
|
|
512
|
+
*/
|
|
513
|
+
export function reactivateMultiFrontierDriver(
|
|
514
|
+
state: MultiFrontierRunState,
|
|
515
|
+
participantId: string,
|
|
516
|
+
): MultiFrontierRunState | null {
|
|
517
|
+
if (
|
|
518
|
+
state.phase !== "paused" ||
|
|
519
|
+
!state.recovery ||
|
|
520
|
+
state.recovery.resumablePhase !== "implementing" ||
|
|
521
|
+
state.approval.state === "pending"
|
|
522
|
+
) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const participant = state.participants.find(
|
|
526
|
+
(candidate) => candidate.participantId === participantId,
|
|
527
|
+
);
|
|
528
|
+
if (!participant || participant.status === "failed") return null;
|
|
529
|
+
|
|
530
|
+
const nextGeneration = (state.driver?.generation ?? 0) + 1;
|
|
531
|
+
return {
|
|
532
|
+
...state,
|
|
533
|
+
phase: state.recovery.resumablePhase,
|
|
534
|
+
participants: state.participants.map((candidate) => ({
|
|
535
|
+
...candidate,
|
|
536
|
+
role: candidate.participantId === participantId ? "driver" : "watchdog",
|
|
537
|
+
permission:
|
|
538
|
+
candidate.participantId === participantId
|
|
539
|
+
? "workspace_write"
|
|
540
|
+
: "read_only",
|
|
541
|
+
status:
|
|
542
|
+
candidate.participantId === participantId
|
|
543
|
+
? "waiting"
|
|
544
|
+
: candidate.status,
|
|
545
|
+
})),
|
|
546
|
+
driver: {
|
|
547
|
+
participantId,
|
|
548
|
+
generation: nextGeneration,
|
|
549
|
+
leaseState: "active",
|
|
550
|
+
},
|
|
551
|
+
recovery: undefined,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Assign the first explicit write lease for an implementing run. */
|
|
556
|
+
export function activateMultiFrontierDriver(
|
|
557
|
+
state: MultiFrontierRunState,
|
|
558
|
+
participantId: string,
|
|
559
|
+
): MultiFrontierRunState | null {
|
|
560
|
+
if (
|
|
561
|
+
state.phase !== "implementing" ||
|
|
562
|
+
state.approval.state === "pending" ||
|
|
563
|
+
state.driver?.leaseState === "active"
|
|
564
|
+
) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const participant = state.participants.find(
|
|
568
|
+
(candidate) => candidate.participantId === participantId,
|
|
569
|
+
);
|
|
570
|
+
if (!participant || participant.status === "failed") return null;
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
...state,
|
|
574
|
+
participants: state.participants.map((candidate) => ({
|
|
575
|
+
...candidate,
|
|
576
|
+
role: candidate.participantId === participantId ? "driver" : "watchdog",
|
|
577
|
+
permission:
|
|
578
|
+
candidate.participantId === participantId
|
|
579
|
+
? "workspace_write"
|
|
580
|
+
: "read_only",
|
|
581
|
+
})),
|
|
582
|
+
driver: {
|
|
583
|
+
participantId,
|
|
584
|
+
generation: (state.driver?.generation ?? 0) + 1,
|
|
585
|
+
leaseState: "active",
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Persist the first explicit driver activation from the main-process coordinator. */
|
|
591
|
+
export function activateStoredMultiFrontierDriver(
|
|
592
|
+
collaborationId: string,
|
|
593
|
+
participantId: string,
|
|
594
|
+
now = new Date().toISOString(),
|
|
595
|
+
): MultiFrontierStoredRun | null {
|
|
596
|
+
assertTimestamp(now, "activation time");
|
|
597
|
+
return mutateStoredMultiFrontierRun(collaborationId, now, (current) =>
|
|
598
|
+
activateMultiFrontierDriver(current, participantId),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Persist an explicit driver reactivation after the human-approved resume. */
|
|
603
|
+
export function reactivateStoredMultiFrontierDriver(
|
|
604
|
+
collaborationId: string,
|
|
605
|
+
participantId: string,
|
|
606
|
+
now = new Date().toISOString(),
|
|
607
|
+
): MultiFrontierStoredRun | null {
|
|
608
|
+
assertTimestamp(now, "reactivation time");
|
|
609
|
+
return mutateStoredMultiFrontierRun(collaborationId, now, (current) =>
|
|
610
|
+
reactivateMultiFrontierDriver(current, participantId),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Appends an idempotent participant event after the coordinator has fenced it.
|
|
616
|
+
* The event journal is diagnostic; state is mutated only through this guarded
|
|
617
|
+
* coordinator entry point.
|
|
618
|
+
*/
|
|
619
|
+
export function appendMultiFrontierParticipantEvent(
|
|
620
|
+
input: AppendMultiFrontierParticipantEventInput,
|
|
621
|
+
): AppendMultiFrontierParticipantEventResult {
|
|
622
|
+
assertSafeId(input.id, "event id");
|
|
623
|
+
assertSafeId(input.collaborationId, "collaboration id");
|
|
624
|
+
assertSafeId(input.participantId, "participant id");
|
|
625
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
626
|
+
assertTimestamp(createdAt, "event time");
|
|
627
|
+
|
|
628
|
+
const runPath = multiFrontierRunPath(input.collaborationId);
|
|
629
|
+
return withFileLockSync(runPath, () => {
|
|
630
|
+
const current = readStoredRun(runPath);
|
|
631
|
+
if (!current) return { accepted: false, reason: "missing-run", run: null };
|
|
632
|
+
const existing = listMultiFrontierParticipantEvents(
|
|
633
|
+
input.collaborationId,
|
|
634
|
+
).find((event) => event.id === input.id);
|
|
635
|
+
if (existing) {
|
|
636
|
+
if (!isMatchingParticipantEvent(existing, input)) {
|
|
637
|
+
return { accepted: false, reason: "event-conflict", run: current };
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
accepted: true,
|
|
641
|
+
deduplicated: true,
|
|
642
|
+
event: existing,
|
|
643
|
+
run: current,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (
|
|
647
|
+
!current.participants.some(
|
|
648
|
+
(candidate) => candidate.participantId === input.participantId,
|
|
649
|
+
)
|
|
650
|
+
) {
|
|
651
|
+
return { accepted: false, reason: "missing-participant", run: current };
|
|
652
|
+
}
|
|
653
|
+
if (!canApplyMultiFrontierParticipantEvent(current, input)) {
|
|
654
|
+
return { accepted: false, reason: "stale-driver", run: current };
|
|
655
|
+
}
|
|
656
|
+
const participant = current.participants.find(
|
|
657
|
+
(candidate) => candidate.participantId === input.participantId,
|
|
658
|
+
);
|
|
659
|
+
if (
|
|
660
|
+
input.status !== undefined &&
|
|
661
|
+
(participant?.status === "completed" ||
|
|
662
|
+
participant?.status === "failed") &&
|
|
663
|
+
input.status !== participant.status
|
|
664
|
+
) {
|
|
665
|
+
return { accepted: false, reason: "terminal-participant", run: current };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const event: PersistedMultiFrontierParticipantEvent = {
|
|
669
|
+
schemaVersion: 1,
|
|
670
|
+
id: input.id,
|
|
671
|
+
collaborationId: input.collaborationId,
|
|
672
|
+
participantId: input.participantId,
|
|
673
|
+
permission: input.permission,
|
|
674
|
+
createdAt,
|
|
675
|
+
...(input.generation === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: { generation: input.generation }),
|
|
678
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
679
|
+
};
|
|
680
|
+
appendParticipantEvent(event);
|
|
681
|
+
compactParticipantEventJournal(input.collaborationId);
|
|
682
|
+
const status = input.status;
|
|
683
|
+
const participants = status
|
|
684
|
+
? current.participants.map((candidate) =>
|
|
685
|
+
candidate.participantId === input.participantId
|
|
686
|
+
? { ...candidate, status }
|
|
687
|
+
: candidate,
|
|
688
|
+
)
|
|
689
|
+
: current.participants;
|
|
690
|
+
const run = writeNextStoredRun(
|
|
691
|
+
current,
|
|
692
|
+
{ ...current, participants },
|
|
693
|
+
createdAt,
|
|
694
|
+
);
|
|
695
|
+
return { accepted: true, deduplicated: false, event, run };
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Appends a bounded proposal, review, or checkpoint index record. The
|
|
701
|
+
* coordinator owns both the artifact and its run-state reference under one
|
|
702
|
+
* run lock, so a reader never observes a durable reference without its record.
|
|
703
|
+
*/
|
|
704
|
+
export function appendMultiFrontierArtifact(
|
|
705
|
+
input: AppendMultiFrontierArtifactInput,
|
|
706
|
+
): AppendMultiFrontierArtifactResult {
|
|
707
|
+
assertArtifactInput(input);
|
|
708
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
709
|
+
assertTimestamp(createdAt, "artifact time");
|
|
710
|
+
const artifact = toPersistedArtifact(input, createdAt);
|
|
711
|
+
const runPath = multiFrontierRunPath(input.collaborationId);
|
|
712
|
+
return withFileLockSync(runPath, () => {
|
|
713
|
+
const current = readStoredRun(runPath);
|
|
714
|
+
if (!current) return { accepted: false, reason: "missing-run", run: null };
|
|
715
|
+
if (
|
|
716
|
+
input.participantId !== undefined &&
|
|
717
|
+
!current.participants.some(
|
|
718
|
+
(participant) => participant.participantId === input.participantId,
|
|
719
|
+
)
|
|
720
|
+
) {
|
|
721
|
+
return { accepted: false, reason: "missing-participant", run: current };
|
|
722
|
+
}
|
|
723
|
+
if (
|
|
724
|
+
input.supersedesArtifactId !== undefined &&
|
|
725
|
+
!readStoredArtifact(
|
|
726
|
+
multiFrontierArtifactPath(
|
|
727
|
+
input.collaborationId,
|
|
728
|
+
input.supersedesArtifactId,
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
) {
|
|
732
|
+
return {
|
|
733
|
+
accepted: false,
|
|
734
|
+
reason: "missing-superseded-artifact",
|
|
735
|
+
run: current,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const artifactPath = multiFrontierArtifactPath(
|
|
740
|
+
input.collaborationId,
|
|
741
|
+
input.id,
|
|
742
|
+
);
|
|
743
|
+
const existing = readStoredArtifact(artifactPath);
|
|
744
|
+
if (existing) {
|
|
745
|
+
if (!isMatchingArtifact(existing, artifact)) {
|
|
746
|
+
return { accepted: false, reason: "artifact-conflict", run: current };
|
|
747
|
+
}
|
|
748
|
+
const ids = artifactIdsForKind(current, existing.kind);
|
|
749
|
+
const run = ids.includes(existing.id)
|
|
750
|
+
? current
|
|
751
|
+
: writeNextStoredRun(
|
|
752
|
+
current,
|
|
753
|
+
withArtifactId(current, existing.kind, existing.id),
|
|
754
|
+
new Date().toISOString(),
|
|
755
|
+
);
|
|
756
|
+
return {
|
|
757
|
+
accepted: true,
|
|
758
|
+
deduplicated: true,
|
|
759
|
+
artifact: existing,
|
|
760
|
+
run,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (fs.existsSync(artifactPath)) {
|
|
764
|
+
return { accepted: false, reason: "artifact-conflict", run: current };
|
|
765
|
+
}
|
|
766
|
+
if (
|
|
767
|
+
listMultiFrontierArtifacts(input.collaborationId).length >=
|
|
768
|
+
MAX_MULTI_FRONTIER_ARTIFACTS_PER_RUN
|
|
769
|
+
) {
|
|
770
|
+
return {
|
|
771
|
+
accepted: false,
|
|
772
|
+
reason: "artifact-limit-reached",
|
|
773
|
+
run: current,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
writeJsonFileAtomically(artifactPath, artifact, { mode: 0o600 });
|
|
778
|
+
const ids = artifactIdsForKind(current, artifact.kind);
|
|
779
|
+
const next = ids.includes(artifact.id)
|
|
780
|
+
? current
|
|
781
|
+
: withArtifactId(current, artifact.kind, artifact.id);
|
|
782
|
+
const run = writeNextStoredRun(current, next, createdAt);
|
|
783
|
+
return { accepted: true, deduplicated: false, artifact, run };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function getMultiFrontierArtifact(
|
|
788
|
+
collaborationId: string,
|
|
789
|
+
artifactId: string,
|
|
790
|
+
): PersistedMultiFrontierArtifact | null {
|
|
791
|
+
if (!SAFE_ID.test(collaborationId) || !SAFE_ID.test(artifactId)) return null;
|
|
792
|
+
return readStoredArtifact(
|
|
793
|
+
multiFrontierArtifactPath(collaborationId, artifactId),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function listMultiFrontierArtifacts(
|
|
798
|
+
collaborationId: string,
|
|
799
|
+
): PersistedMultiFrontierArtifact[] {
|
|
800
|
+
if (!SAFE_ID.test(collaborationId)) return [];
|
|
801
|
+
const directory = multiFrontierArtifactsRunDir(collaborationId);
|
|
802
|
+
if (!fs.existsSync(directory)) return [];
|
|
803
|
+
return fs
|
|
804
|
+
.readdirSync(directory)
|
|
805
|
+
.filter((file) => file.endsWith(".json"))
|
|
806
|
+
.map((file) => readStoredArtifact(path.join(directory, file)))
|
|
807
|
+
.filter(
|
|
808
|
+
(artifact): artifact is PersistedMultiFrontierArtifact =>
|
|
809
|
+
artifact !== null,
|
|
810
|
+
)
|
|
811
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function listMultiFrontierParticipantEvents(
|
|
815
|
+
collaborationId: string,
|
|
816
|
+
): PersistedMultiFrontierParticipantEvent[] {
|
|
817
|
+
if (!SAFE_ID.test(collaborationId)) return [];
|
|
818
|
+
const eventPath = multiFrontierParticipantEventsPath(collaborationId);
|
|
819
|
+
if (!fs.existsSync(eventPath)) return [];
|
|
820
|
+
return fs
|
|
821
|
+
.readFileSync(eventPath, "utf-8")
|
|
822
|
+
.split(/\r?\n/)
|
|
823
|
+
.filter(Boolean)
|
|
824
|
+
.map(readParticipantEvent)
|
|
825
|
+
.filter(
|
|
826
|
+
(event): event is PersistedMultiFrontierParticipantEvent =>
|
|
827
|
+
event !== null,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function getMultiFrontierParticipantEventRetention(
|
|
832
|
+
collaborationId: string,
|
|
833
|
+
): MultiFrontierParticipantEventRetention | null {
|
|
834
|
+
if (!SAFE_ID.test(collaborationId)) return null;
|
|
835
|
+
return readParticipantEventRetention(
|
|
836
|
+
multiFrontierParticipantEventRetentionPath(collaborationId),
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Coordinator maintenance for a bounded event journal. It deliberately keeps
|
|
842
|
+
* the newest contiguous tail and a replay marker instead of deleting run state.
|
|
843
|
+
*/
|
|
844
|
+
export function compactMultiFrontierParticipantEvents(
|
|
845
|
+
collaborationId: string,
|
|
846
|
+
limits = DEFAULT_MULTI_FRONTIER_PARTICIPANT_EVENT_RETENTION,
|
|
847
|
+
): MultiFrontierParticipantEventRetention | null {
|
|
848
|
+
assertSafeId(collaborationId, "collaboration id");
|
|
849
|
+
assertRetentionLimits(limits);
|
|
850
|
+
const runPath = multiFrontierRunPath(collaborationId);
|
|
851
|
+
return withFileLockSync(runPath, () => {
|
|
852
|
+
if (!readStoredRun(runPath)) return null;
|
|
853
|
+
return compactParticipantEventJournal(collaborationId, limits);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function writeMultiFrontierRun(run: MultiFrontierStoredRun): void {
|
|
858
|
+
writeJsonFileAtomically(multiFrontierRunPath(run.collaborationId), run);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function writeNewMultiFrontierRun(run: MultiFrontierStoredRun): void {
|
|
862
|
+
const filePath = multiFrontierRunPath(run.collaborationId);
|
|
863
|
+
withFileLockSync(filePath, () => {
|
|
864
|
+
if (fs.existsSync(filePath)) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
`Multi-frontier run already exists: ${run.collaborationId}`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
writeJsonFileAtomically(filePath, run);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function writeNextStoredRun(
|
|
874
|
+
current: MultiFrontierStoredRun,
|
|
875
|
+
nextState: MultiFrontierRunState,
|
|
876
|
+
updatedAt: string,
|
|
877
|
+
): MultiFrontierStoredRun {
|
|
878
|
+
const next: MultiFrontierStoredRun = {
|
|
879
|
+
...nextState,
|
|
880
|
+
createdAt: current.createdAt,
|
|
881
|
+
updatedAt,
|
|
882
|
+
};
|
|
883
|
+
writeMultiFrontierRun(next);
|
|
884
|
+
return next;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function appendParticipantEvent(
|
|
888
|
+
event: PersistedMultiFrontierParticipantEvent,
|
|
889
|
+
): void {
|
|
890
|
+
appendUniqueJsonLineAtomically(
|
|
891
|
+
multiFrontierParticipantEventsPath(event.collaborationId),
|
|
892
|
+
event,
|
|
893
|
+
readParticipantEventValue,
|
|
894
|
+
{ mode: 0o600 },
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function compactParticipantEventJournal(
|
|
899
|
+
collaborationId: string,
|
|
900
|
+
limits = DEFAULT_MULTI_FRONTIER_PARTICIPANT_EVENT_RETENTION,
|
|
901
|
+
): MultiFrontierParticipantEventRetention {
|
|
902
|
+
const eventPath = multiFrontierParticipantEventsPath(collaborationId);
|
|
903
|
+
return withFileLockSync(eventPath, () => {
|
|
904
|
+
const previous = readParticipantEventRetention(
|
|
905
|
+
multiFrontierParticipantEventRetentionPath(collaborationId),
|
|
906
|
+
);
|
|
907
|
+
const events = listMultiFrontierParticipantEvents(collaborationId);
|
|
908
|
+
const retained: PersistedMultiFrontierParticipantEvent[] = [];
|
|
909
|
+
let retainedByteCount = 0;
|
|
910
|
+
for (const event of [...events].reverse()) {
|
|
911
|
+
const bytes = eventLineByteLength(event);
|
|
912
|
+
if (
|
|
913
|
+
retained.length >= limits.maxEventCount ||
|
|
914
|
+
retainedByteCount + bytes > limits.maxBytes
|
|
915
|
+
) {
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
retained.push(event);
|
|
919
|
+
retainedByteCount += bytes;
|
|
920
|
+
}
|
|
921
|
+
retained.reverse();
|
|
922
|
+
const retainedIds = new Set(retained.map((event) => event.id));
|
|
923
|
+
const dropped = events.filter((event) => !retainedIds.has(event.id));
|
|
924
|
+
if (dropped.length > 0) {
|
|
925
|
+
writeTextFileAtomically(
|
|
926
|
+
eventPath,
|
|
927
|
+
retained.map((event) => `${JSON.stringify(event)}\n`).join(""),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const retention: MultiFrontierParticipantEventRetention = {
|
|
931
|
+
schemaVersion: 1,
|
|
932
|
+
retainedEventCount: retained.length,
|
|
933
|
+
retainedByteCount,
|
|
934
|
+
droppedEventCount: (previous?.droppedEventCount ?? 0) + dropped.length,
|
|
935
|
+
droppedByteCount:
|
|
936
|
+
(previous?.droppedByteCount ?? 0) +
|
|
937
|
+
dropped.reduce((total, event) => total + eventLineByteLength(event), 0),
|
|
938
|
+
truncated: (previous?.truncated ?? false) || dropped.length > 0,
|
|
939
|
+
replay: {
|
|
940
|
+
requiresSnapshot: (previous?.truncated ?? false) || dropped.length > 0,
|
|
941
|
+
...(retained[0] ? { firstRetainedEventId: retained[0].id } : {}),
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
writeJsonFileAtomically(
|
|
945
|
+
multiFrontierParticipantEventRetentionPath(collaborationId),
|
|
946
|
+
retention,
|
|
947
|
+
{ mode: 0o600 },
|
|
948
|
+
);
|
|
949
|
+
return retention;
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function mutateStoredMultiFrontierRun(
|
|
954
|
+
collaborationId: string,
|
|
955
|
+
updatedAt: string,
|
|
956
|
+
mutate: (current: MultiFrontierStoredRun) => MultiFrontierRunState | null,
|
|
957
|
+
): MultiFrontierStoredRun | null {
|
|
958
|
+
const filePath = multiFrontierRunPath(collaborationId);
|
|
959
|
+
return withFileLockSync(filePath, () => {
|
|
960
|
+
const current = readStoredRun(filePath);
|
|
961
|
+
if (!current) return null;
|
|
962
|
+
const nextState = mutate(current);
|
|
963
|
+
if (!nextState) return null;
|
|
964
|
+
const next: MultiFrontierStoredRun = {
|
|
965
|
+
...nextState,
|
|
966
|
+
createdAt: current.createdAt,
|
|
967
|
+
updatedAt,
|
|
968
|
+
};
|
|
969
|
+
writeMultiFrontierRun(next);
|
|
970
|
+
return next;
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function multiFrontierRunPath(collaborationId: string): string {
|
|
975
|
+
return path.join(multiFrontierRunsDir(), `${collaborationId}.json`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function multiFrontierParticipantEventsPath(collaborationId: string): string {
|
|
979
|
+
return path.join(
|
|
980
|
+
multiFrontierParticipantEventsDir(),
|
|
981
|
+
`${collaborationId}.jsonl`,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function multiFrontierParticipantEventRetentionPath(
|
|
986
|
+
collaborationId: string,
|
|
987
|
+
): string {
|
|
988
|
+
return path.join(
|
|
989
|
+
multiFrontierParticipantEventsDir(),
|
|
990
|
+
`${collaborationId}.retention.json`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function multiFrontierArtifactsRunDir(collaborationId: string): string {
|
|
995
|
+
return path.join(multiFrontierArtifactsDir(), collaborationId);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function multiFrontierArtifactPath(
|
|
999
|
+
collaborationId: string,
|
|
1000
|
+
artifactId: string,
|
|
1001
|
+
): string {
|
|
1002
|
+
return path.join(
|
|
1003
|
+
multiFrontierArtifactsRunDir(collaborationId),
|
|
1004
|
+
`${artifactId}.json`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function readStoredRun(filePath: string): MultiFrontierStoredRun | null {
|
|
1009
|
+
try {
|
|
1010
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
1011
|
+
const normalized = normalizeStoredRunDefaults(raw);
|
|
1012
|
+
return isCompleteMultiFrontierStoredRun(normalized) ? normalized : null;
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function isCompleteMultiFrontierStoredRun(
|
|
1019
|
+
value: unknown,
|
|
1020
|
+
): value is MultiFrontierStoredRun {
|
|
1021
|
+
if (!hasStoredRunTimestamps(value)) return false;
|
|
1022
|
+
return (
|
|
1023
|
+
isCompleteMultiFrontierRunState(value) &&
|
|
1024
|
+
!Number.isNaN(Date.parse(value.createdAt)) &&
|
|
1025
|
+
!Number.isNaN(Date.parse(value.updatedAt))
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function isCompleteMultiFrontierRunState(
|
|
1030
|
+
value: unknown,
|
|
1031
|
+
): value is MultiFrontierRunState {
|
|
1032
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1033
|
+
const run = value as Partial<MultiFrontierRunState>;
|
|
1034
|
+
return (
|
|
1035
|
+
run.schemaVersion === 1 &&
|
|
1036
|
+
typeof run.collaborationId === "string" &&
|
|
1037
|
+
SAFE_ID.test(run.collaborationId) &&
|
|
1038
|
+
(run.workspaceId === undefined ||
|
|
1039
|
+
(typeof run.workspaceId === "string" && SAFE_ID.test(run.workspaceId))) &&
|
|
1040
|
+
isPhase(run.phase) &&
|
|
1041
|
+
isStoredRunParticipantsAndDriverValid(run.participants, run.driver) &&
|
|
1042
|
+
isApproval(run.approval) &&
|
|
1043
|
+
Array.isArray(run.checkpointIds) &&
|
|
1044
|
+
run.checkpointIds.every(
|
|
1045
|
+
(checkpointId) => typeof checkpointId === "string",
|
|
1046
|
+
) &&
|
|
1047
|
+
typeof run.round === "number" &&
|
|
1048
|
+
Number.isInteger(run.round) &&
|
|
1049
|
+
run.round >= 1 &&
|
|
1050
|
+
isStringArray(run.proposalIds) &&
|
|
1051
|
+
isStringArray(run.reviewIds) &&
|
|
1052
|
+
typeof run.autoContinueAfterAgreement === "boolean" &&
|
|
1053
|
+
(run.recovery === undefined || isRecovery(run.recovery))
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function normalizeStoredRunDefaults(value: unknown): unknown {
|
|
1058
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1059
|
+
if (!Object.hasOwn(value, "autoContinueAfterAgreement")) {
|
|
1060
|
+
return { ...value, autoContinueAfterAgreement: false };
|
|
1061
|
+
}
|
|
1062
|
+
return value;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function hasStoredRunTimestamps(
|
|
1066
|
+
value: unknown,
|
|
1067
|
+
): value is MultiFrontierStoredRun {
|
|
1068
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1069
|
+
const run = value as Partial<MultiFrontierStoredRun>;
|
|
1070
|
+
return typeof run.createdAt === "string" && typeof run.updatedAt === "string";
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function cloneStoredMultiFrontierRun(
|
|
1074
|
+
run: MultiFrontierStoredRun,
|
|
1075
|
+
): MultiFrontierStoredRun {
|
|
1076
|
+
return JSON.parse(JSON.stringify(run)) as MultiFrontierStoredRun;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
1080
|
+
return (
|
|
1081
|
+
Boolean(value) &&
|
|
1082
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
1083
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function readParticipantEvent(
|
|
1088
|
+
line: string,
|
|
1089
|
+
): PersistedMultiFrontierParticipantEvent | null {
|
|
1090
|
+
try {
|
|
1091
|
+
return readParticipantEventValue(JSON.parse(line) as unknown);
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function readParticipantEventValue(
|
|
1098
|
+
raw: unknown,
|
|
1099
|
+
): PersistedMultiFrontierParticipantEvent | null {
|
|
1100
|
+
try {
|
|
1101
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1102
|
+
const event = raw as Partial<PersistedMultiFrontierParticipantEvent>;
|
|
1103
|
+
if (
|
|
1104
|
+
event.schemaVersion !== 1 ||
|
|
1105
|
+
typeof event.id !== "string" ||
|
|
1106
|
+
typeof event.collaborationId !== "string" ||
|
|
1107
|
+
typeof event.participantId !== "string" ||
|
|
1108
|
+
!isPermission(event.permission) ||
|
|
1109
|
+
(event.generation !== undefined &&
|
|
1110
|
+
typeof event.generation !== "number") ||
|
|
1111
|
+
(event.status !== undefined && !isParticipantStatus(event.status)) ||
|
|
1112
|
+
typeof event.createdAt !== "string" ||
|
|
1113
|
+
Number.isNaN(Date.parse(event.createdAt))
|
|
1114
|
+
) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
return event as PersistedMultiFrontierParticipantEvent;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function readStoredArtifact(
|
|
1124
|
+
filePath: string,
|
|
1125
|
+
): PersistedMultiFrontierArtifact | null {
|
|
1126
|
+
try {
|
|
1127
|
+
return readArtifactValue(
|
|
1128
|
+
JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown,
|
|
1129
|
+
);
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function readArtifactValue(
|
|
1136
|
+
raw: unknown,
|
|
1137
|
+
): PersistedMultiFrontierArtifact | null {
|
|
1138
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1139
|
+
const artifact = raw as Partial<PersistedMultiFrontierArtifact>;
|
|
1140
|
+
if (
|
|
1141
|
+
artifact.schemaVersion !== 1 ||
|
|
1142
|
+
!isSafeId(artifact.id) ||
|
|
1143
|
+
!isSafeId(artifact.collaborationId) ||
|
|
1144
|
+
!isArtifactKind(artifact.kind) ||
|
|
1145
|
+
typeof artifact.createdAt !== "string" ||
|
|
1146
|
+
Number.isNaN(Date.parse(artifact.createdAt)) ||
|
|
1147
|
+
(artifact.participantId !== undefined &&
|
|
1148
|
+
!isSafeId(artifact.participantId)) ||
|
|
1149
|
+
!isBoundedText(artifact.title, MAX_ARTIFACT_TITLE_BYTES) ||
|
|
1150
|
+
!isArtifactSummary(artifact.summary) ||
|
|
1151
|
+
(artifact.supersedesArtifactId !== undefined &&
|
|
1152
|
+
!isSafeId(artifact.supersedesArtifactId)) ||
|
|
1153
|
+
(artifact.contentHash !== undefined &&
|
|
1154
|
+
!SAFE_CONTENT_HASH.test(artifact.contentHash)) ||
|
|
1155
|
+
!isArtifactFileRefs(artifact.fileRefs) ||
|
|
1156
|
+
!isArtifactTestSummaries(artifact.testSummary) ||
|
|
1157
|
+
!isOrchestrationArtifact(artifact.orchestration)
|
|
1158
|
+
) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
return artifact as PersistedMultiFrontierArtifact;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function readParticipantEventRetention(
|
|
1165
|
+
filePath: string,
|
|
1166
|
+
): MultiFrontierParticipantEventRetention | null {
|
|
1167
|
+
try {
|
|
1168
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
1169
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1170
|
+
const retention = raw as Partial<MultiFrontierParticipantEventRetention>;
|
|
1171
|
+
if (
|
|
1172
|
+
retention.schemaVersion !== 1 ||
|
|
1173
|
+
!isNonNegativeInteger(retention.retainedEventCount) ||
|
|
1174
|
+
!isNonNegativeInteger(retention.retainedByteCount) ||
|
|
1175
|
+
!isNonNegativeInteger(retention.droppedEventCount) ||
|
|
1176
|
+
!isNonNegativeInteger(retention.droppedByteCount) ||
|
|
1177
|
+
typeof retention.truncated !== "boolean" ||
|
|
1178
|
+
!retention.replay ||
|
|
1179
|
+
typeof retention.replay !== "object" ||
|
|
1180
|
+
typeof retention.replay.requiresSnapshot !== "boolean" ||
|
|
1181
|
+
(retention.replay.firstRetainedEventId !== undefined &&
|
|
1182
|
+
!isSafeId(retention.replay.firstRetainedEventId))
|
|
1183
|
+
) {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
return retention as MultiFrontierParticipantEventRetention;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function assertArtifactInput(input: AppendMultiFrontierArtifactInput): void {
|
|
1193
|
+
assertAllowlistedArtifactInput(input);
|
|
1194
|
+
assertSafeId(input.id, "artifact id");
|
|
1195
|
+
assertSafeId(input.collaborationId, "collaboration id");
|
|
1196
|
+
if (!isArtifactKind(input.kind))
|
|
1197
|
+
throw new Error("Invalid multi-frontier artifact kind.");
|
|
1198
|
+
if (input.participantId !== undefined) {
|
|
1199
|
+
assertSafeId(input.participantId, "participant id");
|
|
1200
|
+
}
|
|
1201
|
+
assertBoundedText(input.title, MAX_ARTIFACT_TITLE_BYTES, "artifact title");
|
|
1202
|
+
if (!isArtifactSummary(input.summary)) {
|
|
1203
|
+
throw new Error("Invalid multi-frontier artifact summary.");
|
|
1204
|
+
}
|
|
1205
|
+
if (input.supersedesArtifactId !== undefined) {
|
|
1206
|
+
assertSafeId(input.supersedesArtifactId, "superseded artifact id");
|
|
1207
|
+
}
|
|
1208
|
+
if (
|
|
1209
|
+
input.contentHash !== undefined &&
|
|
1210
|
+
!SAFE_CONTENT_HASH.test(input.contentHash)
|
|
1211
|
+
) {
|
|
1212
|
+
throw new Error("Invalid multi-frontier artifact content hash.");
|
|
1213
|
+
}
|
|
1214
|
+
if (!isArtifactFileRefs(input.fileRefs)) {
|
|
1215
|
+
throw new Error("Invalid multi-frontier artifact file references.");
|
|
1216
|
+
}
|
|
1217
|
+
if (!isArtifactTestSummaries(input.testSummary)) {
|
|
1218
|
+
throw new Error("Invalid multi-frontier artifact test summary.");
|
|
1219
|
+
}
|
|
1220
|
+
if (!isOrchestrationArtifact(input.orchestration)) {
|
|
1221
|
+
throw new Error("Invalid multi-frontier orchestration artifact.");
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function assertAllowlistedArtifactInput(
|
|
1226
|
+
input: AppendMultiFrontierArtifactInput,
|
|
1227
|
+
): void {
|
|
1228
|
+
const allowed = new Set([
|
|
1229
|
+
"id",
|
|
1230
|
+
"collaborationId",
|
|
1231
|
+
"kind",
|
|
1232
|
+
"createdAt",
|
|
1233
|
+
"participantId",
|
|
1234
|
+
"title",
|
|
1235
|
+
"summary",
|
|
1236
|
+
"supersedesArtifactId",
|
|
1237
|
+
"contentHash",
|
|
1238
|
+
"fileRefs",
|
|
1239
|
+
"testSummary",
|
|
1240
|
+
"orchestration",
|
|
1241
|
+
]);
|
|
1242
|
+
if (
|
|
1243
|
+
!input ||
|
|
1244
|
+
typeof input !== "object" ||
|
|
1245
|
+
Array.isArray(input) ||
|
|
1246
|
+
Object.keys(input).some((key) => !allowed.has(key))
|
|
1247
|
+
) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
"Multi-frontier artifacts only accept allowlisted summary fields.",
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function toPersistedArtifact(
|
|
1255
|
+
input: AppendMultiFrontierArtifactInput,
|
|
1256
|
+
createdAt: string,
|
|
1257
|
+
): PersistedMultiFrontierArtifact {
|
|
1258
|
+
return {
|
|
1259
|
+
schemaVersion: 1,
|
|
1260
|
+
id: input.id,
|
|
1261
|
+
collaborationId: input.collaborationId,
|
|
1262
|
+
kind: input.kind,
|
|
1263
|
+
createdAt,
|
|
1264
|
+
title: input.title,
|
|
1265
|
+
summary: input.summary,
|
|
1266
|
+
...(input.participantId === undefined
|
|
1267
|
+
? {}
|
|
1268
|
+
: { participantId: input.participantId }),
|
|
1269
|
+
...(input.supersedesArtifactId === undefined
|
|
1270
|
+
? {}
|
|
1271
|
+
: { supersedesArtifactId: input.supersedesArtifactId }),
|
|
1272
|
+
...(input.contentHash === undefined
|
|
1273
|
+
? {}
|
|
1274
|
+
: { contentHash: input.contentHash }),
|
|
1275
|
+
...(input.fileRefs === undefined ? {} : { fileRefs: [...input.fileRefs] }),
|
|
1276
|
+
...(input.testSummary === undefined
|
|
1277
|
+
? {}
|
|
1278
|
+
: {
|
|
1279
|
+
testSummary: input.testSummary.map((summary) => ({ ...summary })),
|
|
1280
|
+
}),
|
|
1281
|
+
...(input.orchestration === undefined
|
|
1282
|
+
? {}
|
|
1283
|
+
: { orchestration: cloneOrchestrationArtifact(input.orchestration) }),
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function artifactIdsForKind(
|
|
1288
|
+
run: MultiFrontierStoredRun,
|
|
1289
|
+
kind: MultiFrontierArtifactKind,
|
|
1290
|
+
): string[] {
|
|
1291
|
+
if (kind === "proposal") return run.proposalIds;
|
|
1292
|
+
if (kind === "review") return run.reviewIds;
|
|
1293
|
+
return run.checkpointIds;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function withArtifactId(
|
|
1297
|
+
run: MultiFrontierStoredRun,
|
|
1298
|
+
kind: MultiFrontierArtifactKind,
|
|
1299
|
+
artifactId: string,
|
|
1300
|
+
): MultiFrontierStoredRun {
|
|
1301
|
+
if (kind === "proposal") {
|
|
1302
|
+
return { ...run, proposalIds: [...run.proposalIds, artifactId] };
|
|
1303
|
+
}
|
|
1304
|
+
if (kind === "review") {
|
|
1305
|
+
return { ...run, reviewIds: [...run.reviewIds, artifactId] };
|
|
1306
|
+
}
|
|
1307
|
+
return { ...run, checkpointIds: [...run.checkpointIds, artifactId] };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function isMatchingArtifact(
|
|
1311
|
+
existing: PersistedMultiFrontierArtifact,
|
|
1312
|
+
candidate: PersistedMultiFrontierArtifact,
|
|
1313
|
+
): boolean {
|
|
1314
|
+
return JSON.stringify(existing) === JSON.stringify(candidate);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function isArtifactKind(value: unknown): value is MultiFrontierArtifactKind {
|
|
1318
|
+
return value === "proposal" || value === "review" || value === "checkpoint";
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function isArtifactSummary(value: unknown): value is string {
|
|
1322
|
+
return (
|
|
1323
|
+
isBoundedText(value, MAX_ARTIFACT_SUMMARY_BYTES) &&
|
|
1324
|
+
!/(?:^|\n)diff --git\s|(?:^|\n)@@\s+-\d+/m.test(value)
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function isArtifactFileRefs(value: unknown): value is string[] | undefined {
|
|
1329
|
+
return (
|
|
1330
|
+
value === undefined ||
|
|
1331
|
+
(Array.isArray(value) &&
|
|
1332
|
+
value.length <= MAX_ARTIFACT_FILE_REFS &&
|
|
1333
|
+
value.every(
|
|
1334
|
+
(fileRef) =>
|
|
1335
|
+
typeof fileRef === "string" &&
|
|
1336
|
+
Buffer.byteLength(fileRef, "utf-8") <= 512 &&
|
|
1337
|
+
SAFE_FILE_REF.test(fileRef),
|
|
1338
|
+
))
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function isArtifactTestSummaries(
|
|
1343
|
+
value: unknown,
|
|
1344
|
+
): value is MultiFrontierArtifactTestSummary[] | undefined {
|
|
1345
|
+
return (
|
|
1346
|
+
value === undefined ||
|
|
1347
|
+
(Array.isArray(value) &&
|
|
1348
|
+
value.length <= MAX_ARTIFACT_TEST_SUMMARIES &&
|
|
1349
|
+
value.every(
|
|
1350
|
+
(summary) =>
|
|
1351
|
+
Boolean(summary) &&
|
|
1352
|
+
typeof summary === "object" &&
|
|
1353
|
+
isBoundedText(
|
|
1354
|
+
(summary as MultiFrontierArtifactTestSummary).name,
|
|
1355
|
+
MAX_ARTIFACT_TEST_NAME_BYTES,
|
|
1356
|
+
) &&
|
|
1357
|
+
["passed", "failed", "skipped"].includes(
|
|
1358
|
+
(summary as MultiFrontierArtifactTestSummary).status,
|
|
1359
|
+
) &&
|
|
1360
|
+
((summary as MultiFrontierArtifactTestSummary).summary ===
|
|
1361
|
+
undefined ||
|
|
1362
|
+
isBoundedText(
|
|
1363
|
+
(summary as MultiFrontierArtifactTestSummary).summary,
|
|
1364
|
+
MAX_ARTIFACT_TEST_SUMMARY_BYTES,
|
|
1365
|
+
)),
|
|
1366
|
+
))
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function isOrchestrationArtifact(
|
|
1371
|
+
value: unknown,
|
|
1372
|
+
): value is MultiFrontierOrchestrationArtifact | undefined {
|
|
1373
|
+
if (value === undefined) return true;
|
|
1374
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1375
|
+
const artifact = value as Partial<MultiFrontierOrchestrationArtifact>;
|
|
1376
|
+
return (
|
|
1377
|
+
hasExactKeys(artifact, [
|
|
1378
|
+
"id",
|
|
1379
|
+
"kind",
|
|
1380
|
+
"round",
|
|
1381
|
+
"participantId",
|
|
1382
|
+
"text",
|
|
1383
|
+
"attribution",
|
|
1384
|
+
"supersedesArtifactId",
|
|
1385
|
+
"metadata",
|
|
1386
|
+
]) &&
|
|
1387
|
+
isSafeId(artifact.id) &&
|
|
1388
|
+
[
|
|
1389
|
+
"proposal",
|
|
1390
|
+
"cross_review",
|
|
1391
|
+
"revision",
|
|
1392
|
+
"synthesis",
|
|
1393
|
+
"checkpoint",
|
|
1394
|
+
"watchdog_review",
|
|
1395
|
+
"finding_disposition",
|
|
1396
|
+
"completion",
|
|
1397
|
+
].includes(artifact.kind ?? "") &&
|
|
1398
|
+
Number.isSafeInteger(artifact.round) &&
|
|
1399
|
+
artifact.round! >= 1 &&
|
|
1400
|
+
artifact.round! <= 3 &&
|
|
1401
|
+
(artifact.participantId === undefined ||
|
|
1402
|
+
isSafeId(artifact.participantId)) &&
|
|
1403
|
+
isSafeOrchestrationText(artifact.text) &&
|
|
1404
|
+
isOrchestrationAttribution(artifact.attribution) &&
|
|
1405
|
+
(artifact.supersedesArtifactId === undefined ||
|
|
1406
|
+
isSafeId(artifact.supersedesArtifactId)) &&
|
|
1407
|
+
isSafeOrchestrationMetadata(artifact.kind!, artifact.metadata)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function hasExactKeys(value: object, allowed: readonly string[]): boolean {
|
|
1412
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function isOrchestrationAttribution(
|
|
1416
|
+
value: unknown,
|
|
1417
|
+
): value is MultiFrontierOrchestrationArtifact["attribution"] {
|
|
1418
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1419
|
+
const attribution = value as Partial<
|
|
1420
|
+
MultiFrontierOrchestrationArtifact["attribution"]
|
|
1421
|
+
>;
|
|
1422
|
+
return (
|
|
1423
|
+
hasExactKeys(attribution as Record<string, unknown>, [
|
|
1424
|
+
"participantIds",
|
|
1425
|
+
"sourceArtifactIds",
|
|
1426
|
+
]) &&
|
|
1427
|
+
isSafeIdArray(attribution.participantIds, 2) &&
|
|
1428
|
+
isSafeIdArray(
|
|
1429
|
+
attribution.sourceArtifactIds,
|
|
1430
|
+
MAX_MULTI_FRONTIER_ARTIFACTS_PER_RUN,
|
|
1431
|
+
)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function isSafeIdArray(value: unknown, max: number): value is string[] {
|
|
1436
|
+
return (
|
|
1437
|
+
Array.isArray(value) &&
|
|
1438
|
+
value.length <= max &&
|
|
1439
|
+
value.every((item) => isSafeId(item))
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function isSafeOrchestrationText(value: unknown): value is string {
|
|
1444
|
+
return (
|
|
1445
|
+
isArtifactSummary(value) &&
|
|
1446
|
+
!/(?:bearer\s+|api[_-]?key\s*[:=]|authorization\s*[:=]|(?:sk|rk|pk)[_-][A-Za-z0-9_-]{12,}|(?:access|refresh)[_-]?token\s*[:=])/i.test(
|
|
1447
|
+
value,
|
|
1448
|
+
)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function isSafeOrchestrationMetadata(
|
|
1453
|
+
kind: NonNullable<MultiFrontierOrchestrationArtifact["kind"]>,
|
|
1454
|
+
value: unknown,
|
|
1455
|
+
): value is Record<string, unknown> | undefined {
|
|
1456
|
+
const allowedKeys = metadataKeysForOrchestrationKind(kind);
|
|
1457
|
+
if (allowedKeys.length === 0) return value === undefined;
|
|
1458
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1459
|
+
const entries = Object.entries(value);
|
|
1460
|
+
if (
|
|
1461
|
+
entries.length === 0 ||
|
|
1462
|
+
entries.some(([key]) => !allowedKeys.includes(key)) ||
|
|
1463
|
+
entries.some(([key]) =>
|
|
1464
|
+
/(token|secret|authorization|credential|password|email|organization|account)/i.test(
|
|
1465
|
+
key,
|
|
1466
|
+
),
|
|
1467
|
+
)
|
|
1468
|
+
) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
try {
|
|
1472
|
+
return (
|
|
1473
|
+
isArtifactSummary(JSON.stringify(value)) &&
|
|
1474
|
+
isValidOrchestrationMetadata(kind, value as Record<string, unknown>)
|
|
1475
|
+
);
|
|
1476
|
+
} catch {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function isValidOrchestrationMetadata(
|
|
1482
|
+
kind: NonNullable<MultiFrontierOrchestrationArtifact["kind"]>,
|
|
1483
|
+
metadata: Record<string, unknown>,
|
|
1484
|
+
): boolean {
|
|
1485
|
+
if (kind === "synthesis") {
|
|
1486
|
+
return (
|
|
1487
|
+
typeof metadata.agreed === "boolean" &&
|
|
1488
|
+
typeof metadata.deterministicallyResolved === "boolean" &&
|
|
1489
|
+
(metadata.reversibleResolution === undefined ||
|
|
1490
|
+
isReversibleResolution(metadata.reversibleResolution))
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
if (kind === "checkpoint") return isCheckpointBundle(metadata.bundle);
|
|
1494
|
+
if (kind === "cross_review" || kind === "watchdog_review") {
|
|
1495
|
+
return isFindingList(metadata.findings);
|
|
1496
|
+
}
|
|
1497
|
+
if (kind === "finding_disposition") {
|
|
1498
|
+
return isFindingDispositionList(metadata.dispositions);
|
|
1499
|
+
}
|
|
1500
|
+
if (kind === "completion") {
|
|
1501
|
+
return (
|
|
1502
|
+
isCompletionTests(metadata.tests) &&
|
|
1503
|
+
isSafeTextList(metadata.proofRefs, 40) &&
|
|
1504
|
+
isSafeTextList(metadata.remainingRisks, 40)
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function isFindingList(value: unknown): boolean {
|
|
1511
|
+
return (
|
|
1512
|
+
Array.isArray(value) &&
|
|
1513
|
+
value.length <= 40 &&
|
|
1514
|
+
value.every((finding) => {
|
|
1515
|
+
if (!finding || typeof finding !== "object" || Array.isArray(finding))
|
|
1516
|
+
return false;
|
|
1517
|
+
const item = finding as Record<string, unknown>;
|
|
1518
|
+
return (
|
|
1519
|
+
hasExactKeys(item, [
|
|
1520
|
+
"id",
|
|
1521
|
+
"category",
|
|
1522
|
+
"summary",
|
|
1523
|
+
"rawFindingId",
|
|
1524
|
+
"reviewerParticipantId",
|
|
1525
|
+
]) &&
|
|
1526
|
+
isSafeId(item.id) &&
|
|
1527
|
+
isSafeOrchestrationText(item.summary) &&
|
|
1528
|
+
isSafeId(item.rawFindingId) &&
|
|
1529
|
+
isSafeId(item.reviewerParticipantId) &&
|
|
1530
|
+
[
|
|
1531
|
+
"reversible_technical",
|
|
1532
|
+
"intent_or_scope",
|
|
1533
|
+
"destructive_action",
|
|
1534
|
+
"security_or_privacy",
|
|
1535
|
+
"outward_effect",
|
|
1536
|
+
"meaningful_cost_expansion",
|
|
1537
|
+
"irreversible_architecture",
|
|
1538
|
+
].includes(item.category as string)
|
|
1539
|
+
);
|
|
1540
|
+
})
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function isFindingDispositionList(value: unknown): boolean {
|
|
1545
|
+
return (
|
|
1546
|
+
Array.isArray(value) &&
|
|
1547
|
+
value.length <= 40 &&
|
|
1548
|
+
value.every((disposition) => {
|
|
1549
|
+
if (
|
|
1550
|
+
!disposition ||
|
|
1551
|
+
typeof disposition !== "object" ||
|
|
1552
|
+
Array.isArray(disposition)
|
|
1553
|
+
)
|
|
1554
|
+
return false;
|
|
1555
|
+
const item = disposition as Record<string, unknown>;
|
|
1556
|
+
return (
|
|
1557
|
+
hasExactKeys(item, ["findingId", "disposition", "reason"]) &&
|
|
1558
|
+
isSafeId(item.findingId) &&
|
|
1559
|
+
["addressed", "rejected", "deferred"].includes(
|
|
1560
|
+
item.disposition as string,
|
|
1561
|
+
) &&
|
|
1562
|
+
isSafeOrchestrationText(item.reason)
|
|
1563
|
+
);
|
|
1564
|
+
})
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function isReversibleResolution(value: unknown): boolean {
|
|
1569
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1570
|
+
const item = value as Record<string, unknown>;
|
|
1571
|
+
return (
|
|
1572
|
+
hasExactKeys(item, [
|
|
1573
|
+
"alternatives",
|
|
1574
|
+
"comparator",
|
|
1575
|
+
"selected",
|
|
1576
|
+
"reversibility",
|
|
1577
|
+
]) &&
|
|
1578
|
+
isSafeTextList(item.alternatives, 8) &&
|
|
1579
|
+
isSafeOrchestrationText(item.comparator) &&
|
|
1580
|
+
isSafeOrchestrationText(item.selected) &&
|
|
1581
|
+
isSafeOrchestrationText(item.reversibility)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function isCheckpointBundle(value: unknown): boolean {
|
|
1586
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1587
|
+
const item = value as Record<string, unknown>;
|
|
1588
|
+
return (
|
|
1589
|
+
hasExactKeys(item, [
|
|
1590
|
+
"requestSummary",
|
|
1591
|
+
"requestRef",
|
|
1592
|
+
"acceptedPlanArtifactId",
|
|
1593
|
+
"contentRef",
|
|
1594
|
+
"contentHash",
|
|
1595
|
+
"testOutput",
|
|
1596
|
+
"driverSummary",
|
|
1597
|
+
"openRisks",
|
|
1598
|
+
"unresolvedFindingIds",
|
|
1599
|
+
]) &&
|
|
1600
|
+
isSafeOrchestrationText(item.requestSummary) &&
|
|
1601
|
+
(item.requestRef === undefined || isSafeId(item.requestRef)) &&
|
|
1602
|
+
isSafeId(item.acceptedPlanArtifactId) &&
|
|
1603
|
+
typeof item.contentRef === "string" &&
|
|
1604
|
+
/^(?!\/)(?!.*\.\.)[A-Za-z0-9._/@:-]{1,512}$/.test(item.contentRef) &&
|
|
1605
|
+
typeof item.contentHash === "string" &&
|
|
1606
|
+
SAFE_CONTENT_HASH.test(item.contentHash) &&
|
|
1607
|
+
isSafeOrchestrationText(item.testOutput) &&
|
|
1608
|
+
isSafeOrchestrationText(item.driverSummary) &&
|
|
1609
|
+
isSafeTextList(item.openRisks, 40) &&
|
|
1610
|
+
isSafeIdArray(item.unresolvedFindingIds, 40)
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function isCompletionTests(value: unknown): boolean {
|
|
1615
|
+
return (
|
|
1616
|
+
Array.isArray(value) &&
|
|
1617
|
+
value.length <= 40 &&
|
|
1618
|
+
value.every((test) => {
|
|
1619
|
+
if (!test || typeof test !== "object" || Array.isArray(test))
|
|
1620
|
+
return false;
|
|
1621
|
+
const item = test as Record<string, unknown>;
|
|
1622
|
+
return (
|
|
1623
|
+
hasExactKeys(item, ["name", "status", "evidence"]) &&
|
|
1624
|
+
isSafeOrchestrationText(item.name) &&
|
|
1625
|
+
["passed", "failed", "skipped"].includes(item.status as string) &&
|
|
1626
|
+
isSafeOrchestrationText(item.evidence)
|
|
1627
|
+
);
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function isSafeTextList(value: unknown, max: number): boolean {
|
|
1633
|
+
return (
|
|
1634
|
+
Array.isArray(value) &&
|
|
1635
|
+
value.length <= max &&
|
|
1636
|
+
value.every((item) => isSafeOrchestrationText(item))
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function metadataKeysForOrchestrationKind(
|
|
1641
|
+
kind: NonNullable<MultiFrontierOrchestrationArtifact["kind"]>,
|
|
1642
|
+
): string[] {
|
|
1643
|
+
if (kind === "synthesis") {
|
|
1644
|
+
return ["agreed", "deterministicallyResolved", "reversibleResolution"];
|
|
1645
|
+
}
|
|
1646
|
+
if (kind === "checkpoint") return ["bundle"];
|
|
1647
|
+
if (kind === "cross_review" || kind === "watchdog_review") {
|
|
1648
|
+
return ["findings"];
|
|
1649
|
+
}
|
|
1650
|
+
if (kind === "finding_disposition") return ["dispositions"];
|
|
1651
|
+
if (kind === "completion") return ["tests", "proofRefs", "remainingRisks"];
|
|
1652
|
+
return [];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function containsUnsafeOrchestrationValue(
|
|
1656
|
+
value: unknown,
|
|
1657
|
+
depth: number,
|
|
1658
|
+
): boolean {
|
|
1659
|
+
if (depth > 8) return true;
|
|
1660
|
+
if (typeof value === "string") return !isSafeOrchestrationText(value);
|
|
1661
|
+
if (
|
|
1662
|
+
value === null ||
|
|
1663
|
+
typeof value === "boolean" ||
|
|
1664
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
1665
|
+
) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
if (Array.isArray(value)) {
|
|
1669
|
+
return (
|
|
1670
|
+
value.length > 40 ||
|
|
1671
|
+
value.some((item) => containsUnsafeOrchestrationValue(item, depth + 1))
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (!value || typeof value !== "object") return true;
|
|
1675
|
+
const entries = Object.entries(value);
|
|
1676
|
+
return (
|
|
1677
|
+
entries.length > 40 ||
|
|
1678
|
+
entries.some(
|
|
1679
|
+
([key, item]) =>
|
|
1680
|
+
!isBoundedText(key, 160) ||
|
|
1681
|
+
/(token|secret|authorization|credential|password|email|organization|account)/i.test(
|
|
1682
|
+
key,
|
|
1683
|
+
) ||
|
|
1684
|
+
containsUnsafeOrchestrationValue(item, depth + 1),
|
|
1685
|
+
)
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function cloneOrchestrationArtifact(
|
|
1690
|
+
artifact: MultiFrontierOrchestrationArtifact,
|
|
1691
|
+
): MultiFrontierOrchestrationArtifact {
|
|
1692
|
+
return JSON.parse(
|
|
1693
|
+
JSON.stringify(artifact),
|
|
1694
|
+
) as MultiFrontierOrchestrationArtifact;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function isBoundedText(value: unknown, maxBytes: number): value is string {
|
|
1698
|
+
return (
|
|
1699
|
+
typeof value === "string" &&
|
|
1700
|
+
value.trim().length > 0 &&
|
|
1701
|
+
Buffer.byteLength(value, "utf-8") <= maxBytes
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function assertBoundedText(
|
|
1706
|
+
value: unknown,
|
|
1707
|
+
maxBytes: number,
|
|
1708
|
+
label: string,
|
|
1709
|
+
): void {
|
|
1710
|
+
if (!isBoundedText(value, maxBytes)) {
|
|
1711
|
+
throw new Error(`Invalid multi-frontier ${label}.`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function assertRetentionLimits(
|
|
1716
|
+
limits: MultiFrontierParticipantEventRetentionLimits,
|
|
1717
|
+
): void {
|
|
1718
|
+
if (
|
|
1719
|
+
!isNonNegativeInteger(limits.maxEventCount) ||
|
|
1720
|
+
limits.maxEventCount < 1 ||
|
|
1721
|
+
!isNonNegativeInteger(limits.maxBytes) ||
|
|
1722
|
+
limits.maxBytes < 512
|
|
1723
|
+
) {
|
|
1724
|
+
throw new Error(
|
|
1725
|
+
"Invalid multi-frontier participant event retention limits.",
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function eventLineByteLength(
|
|
1731
|
+
event: PersistedMultiFrontierParticipantEvent,
|
|
1732
|
+
): number {
|
|
1733
|
+
return Buffer.byteLength(`${JSON.stringify(event)}\n`, "utf-8");
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function isSafeId(value: unknown): value is string {
|
|
1737
|
+
return typeof value === "string" && SAFE_ID.test(value);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
1741
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function assertParticipants(
|
|
1745
|
+
participants: MultiFrontierParticipantState[],
|
|
1746
|
+
): void {
|
|
1747
|
+
if (participants.length === 0) {
|
|
1748
|
+
throw new Error("A multi-frontier run needs at least one participant.");
|
|
1749
|
+
}
|
|
1750
|
+
const participantIds = new Set<string>();
|
|
1751
|
+
for (const participant of participants) {
|
|
1752
|
+
if (!isParticipant(participant))
|
|
1753
|
+
throw new Error("Invalid multi-frontier participant.");
|
|
1754
|
+
assertSafeId(participant.participantId, "participant id");
|
|
1755
|
+
if (participantIds.has(participant.participantId)) {
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
`Duplicate multi-frontier participant id: ${participant.participantId}`,
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
participantIds.add(participant.participantId);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function isParticipant(value: unknown): value is MultiFrontierParticipantState {
|
|
1765
|
+
if (!value || typeof value !== "object") return false;
|
|
1766
|
+
const participant = value as Partial<MultiFrontierParticipantState>;
|
|
1767
|
+
return (
|
|
1768
|
+
typeof participant.participantId === "string" &&
|
|
1769
|
+
SAFE_ID.test(participant.participantId) &&
|
|
1770
|
+
typeof participant.provider === "string" &&
|
|
1771
|
+
participant.provider.length > 0 &&
|
|
1772
|
+
typeof participant.runtime === "string" &&
|
|
1773
|
+
participant.runtime.length > 0 &&
|
|
1774
|
+
(participant.model === undefined ||
|
|
1775
|
+
typeof participant.model === "string") &&
|
|
1776
|
+
(participant.capabilities === undefined ||
|
|
1777
|
+
isStringArray(participant.capabilities)) &&
|
|
1778
|
+
(participant.sessionRef === undefined ||
|
|
1779
|
+
typeof participant.sessionRef === "string") &&
|
|
1780
|
+
(participant.role === "driver" || participant.role === "watchdog") &&
|
|
1781
|
+
isPermission(participant.permission) &&
|
|
1782
|
+
isParticipantStatus(participant.status)
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function areStoredParticipantsValid(
|
|
1787
|
+
value: unknown,
|
|
1788
|
+
): value is MultiFrontierParticipantState[] {
|
|
1789
|
+
if (
|
|
1790
|
+
!Array.isArray(value) ||
|
|
1791
|
+
value.length === 0 ||
|
|
1792
|
+
!value.every(isParticipant)
|
|
1793
|
+
) {
|
|
1794
|
+
return false;
|
|
1795
|
+
}
|
|
1796
|
+
return (
|
|
1797
|
+
new Set(value.map((participant) => participant.participantId)).size ===
|
|
1798
|
+
value.length
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function isStoredRunParticipantsAndDriverValid(
|
|
1803
|
+
participants: unknown,
|
|
1804
|
+
driver: unknown,
|
|
1805
|
+
): participants is MultiFrontierParticipantState[] {
|
|
1806
|
+
if (!areStoredParticipantsValid(participants) || !isDriver(driver)) {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
if (driver?.leaseState !== "active") {
|
|
1810
|
+
return participants.every(
|
|
1811
|
+
(participant) => participant.permission === "read_only",
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
if (
|
|
1815
|
+
!participants.some(
|
|
1816
|
+
(participant) => participant.participantId === driver.participantId,
|
|
1817
|
+
)
|
|
1818
|
+
) {
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
return participants.every((participant) => {
|
|
1822
|
+
if (participant.participantId !== driver.participantId) {
|
|
1823
|
+
return (
|
|
1824
|
+
participant.role === "watchdog" &&
|
|
1825
|
+
participant.permission === "read_only"
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
return (
|
|
1829
|
+
participant.role === "driver" &&
|
|
1830
|
+
participant.permission === "workspace_write"
|
|
1831
|
+
);
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function isDriver(value: unknown): value is MultiFrontierDriverLease | null {
|
|
1836
|
+
if (value === null) return true;
|
|
1837
|
+
if (!value || typeof value !== "object") return false;
|
|
1838
|
+
const driver = value as Partial<MultiFrontierDriverLease>;
|
|
1839
|
+
return (
|
|
1840
|
+
typeof driver.participantId === "string" &&
|
|
1841
|
+
typeof driver.generation === "number" &&
|
|
1842
|
+
(driver.leaseState === "inactive" ||
|
|
1843
|
+
driver.leaseState === "active" ||
|
|
1844
|
+
driver.leaseState === "revoked")
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function isApproval(value: unknown): value is MultiFrontierApproval {
|
|
1849
|
+
if (!value || typeof value !== "object") return false;
|
|
1850
|
+
const approval = value as Partial<MultiFrontierApproval>;
|
|
1851
|
+
return (
|
|
1852
|
+
(approval.state === "not_required" ||
|
|
1853
|
+
approval.state === "pending" ||
|
|
1854
|
+
approval.state === "approved" ||
|
|
1855
|
+
approval.state === "rejected") &&
|
|
1856
|
+
(approval.proposalId === undefined ||
|
|
1857
|
+
typeof approval.proposalId === "string") &&
|
|
1858
|
+
(approval.reviewPacketId === undefined ||
|
|
1859
|
+
typeof approval.reviewPacketId === "string")
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function isRecovery(value: unknown): value is MultiFrontierRecovery {
|
|
1864
|
+
if (!value || typeof value !== "object") return false;
|
|
1865
|
+
const recovery = value as Partial<MultiFrontierRecovery>;
|
|
1866
|
+
return (
|
|
1867
|
+
isRecoveryReason(recovery.reason) &&
|
|
1868
|
+
typeof recovery.recoveredAt === "string" &&
|
|
1869
|
+
!Number.isNaN(Date.parse(recovery.recoveredAt)) &&
|
|
1870
|
+
isPhase(recovery.resumablePhase) &&
|
|
1871
|
+
(recovery.checkpointId === undefined ||
|
|
1872
|
+
typeof recovery.checkpointId === "string")
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function isPhase(value: unknown): value is MultiFrontierPhase {
|
|
1877
|
+
return (
|
|
1878
|
+
typeof value === "string" &&
|
|
1879
|
+
[
|
|
1880
|
+
"proposing",
|
|
1881
|
+
"cross_review",
|
|
1882
|
+
"converging",
|
|
1883
|
+
"awaiting_go",
|
|
1884
|
+
"implementing",
|
|
1885
|
+
"checkpoint_review",
|
|
1886
|
+
"paused",
|
|
1887
|
+
"completed",
|
|
1888
|
+
"failed",
|
|
1889
|
+
"canceled",
|
|
1890
|
+
].includes(value)
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function isParticipantStatus(
|
|
1895
|
+
value: unknown,
|
|
1896
|
+
): value is MultiFrontierParticipantStatus {
|
|
1897
|
+
return (
|
|
1898
|
+
value === "idle" ||
|
|
1899
|
+
value === "running" ||
|
|
1900
|
+
value === "waiting" ||
|
|
1901
|
+
value === "failed" ||
|
|
1902
|
+
value === "completed"
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function isPermission(
|
|
1907
|
+
value: unknown,
|
|
1908
|
+
): value is MultiFrontierParticipantPermission {
|
|
1909
|
+
return value === "read_only" || value === "workspace_write";
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function isMatchingParticipantEvent(
|
|
1913
|
+
existing: PersistedMultiFrontierParticipantEvent,
|
|
1914
|
+
input: AppendMultiFrontierParticipantEventInput,
|
|
1915
|
+
): boolean {
|
|
1916
|
+
return (
|
|
1917
|
+
existing.collaborationId === input.collaborationId &&
|
|
1918
|
+
existing.participantId === input.participantId &&
|
|
1919
|
+
existing.permission === input.permission &&
|
|
1920
|
+
existing.generation === input.generation &&
|
|
1921
|
+
existing.status === input.status &&
|
|
1922
|
+
(input.createdAt === undefined || existing.createdAt === input.createdAt)
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function isStringArray(value: unknown): value is string[] {
|
|
1927
|
+
return (
|
|
1928
|
+
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function isRecoveryReason(
|
|
1933
|
+
value: unknown,
|
|
1934
|
+
): value is MultiFrontierRecoveryReason {
|
|
1935
|
+
return (
|
|
1936
|
+
value === "main_process_restarted" ||
|
|
1937
|
+
value === "driver_crashed" ||
|
|
1938
|
+
value === "watchdog_crashed" ||
|
|
1939
|
+
value === "app_quit" ||
|
|
1940
|
+
value === "canceled"
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function assertSafeId(value: string, label: string): void {
|
|
1945
|
+
if (!SAFE_ID.test(value)) throw new Error(`Invalid multi-frontier ${label}.`);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function assertTimestamp(value: string, label: string): void {
|
|
1949
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
1950
|
+
throw new Error(`Invalid multi-frontier ${label}.`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|