@agent-native/core 0.112.0 → 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 +22 -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 +6 -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 +18 -10
- package/corpus/core/src/cli/code-agent-runs.concurrent-worker.ts +33 -0
- package/corpus/core/src/cli/code-agent-runs.ts +55 -62
- package/corpus/core/src/cli/codex-cli-participant.ts +496 -0
- package/corpus/core/src/cli/multi-frontier-runs.ts +1246 -135
- package/corpus/core/src/client/chat/ChatHistoryList.tsx +7 -404
- 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/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/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/actions/_schemas.ts +13 -7
- package/corpus/templates/brain/actions/delete-staged-dataset.ts +3 -24
- package/corpus/templates/brain/actions/list-staged-datasets.ts +3 -28
- 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/update-source.ts +5 -1
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +48 -154
- package/corpus/templates/brain/app/lib/brain.ts +55 -0
- package/corpus/templates/brain/app/lib/ingest-handoff.ts +35 -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/server/lib/connectors.ts +53 -2
- package/corpus/templates/brain/server/lib/slack-channel-membership.ts +44 -0
- package/corpus/templates/brain/shared/slack-source-config.ts +60 -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.map +1 -1
- package/dist/cli/code-agent-executor.js +18 -10
- 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.map +1 -1
- package/dist/cli/code-agent-runs.js +43 -56
- 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 +115 -0
- package/dist/cli/multi-frontier-runs.d.ts.map +1 -1
- package/dist/cli/multi-frontier-runs.js +831 -113
- package/dist/cli/multi-frontier-runs.js.map +1 -1
- 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/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/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/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/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 +7 -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
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
4
|
+
import {
|
|
5
|
+
appendUniqueJsonLineAtomically,
|
|
6
|
+
withFileLockSync,
|
|
7
|
+
writeJsonFileAtomically,
|
|
8
|
+
writeTextFileAtomically,
|
|
9
|
+
} from "./atomic-json-file.js";
|
|
5
10
|
import { codeAgentStoreRoot } from "./code-agent-runs.js";
|
|
6
11
|
|
|
7
12
|
export type MultiFrontierPhase =
|
|
@@ -76,6 +81,8 @@ export interface MultiFrontierRecovery {
|
|
|
76
81
|
export interface MultiFrontierRunState {
|
|
77
82
|
schemaVersion: 1;
|
|
78
83
|
collaborationId: string;
|
|
84
|
+
/** Opaque local workspace handle; never a filesystem path. */
|
|
85
|
+
workspaceId?: string;
|
|
79
86
|
phase: MultiFrontierPhase;
|
|
80
87
|
participants: MultiFrontierParticipantState[];
|
|
81
88
|
driver: MultiFrontierDriverLease | null;
|
|
@@ -84,15 +91,19 @@ export interface MultiFrontierRunState {
|
|
|
84
91
|
round: number;
|
|
85
92
|
proposalIds: string[];
|
|
86
93
|
reviewIds: string[];
|
|
94
|
+
/** Opt-in policy; explicit GO remains the durable default. */
|
|
95
|
+
autoContinueAfterAgreement: boolean;
|
|
87
96
|
recovery?: MultiFrontierRecovery;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
export interface CreateMultiFrontierRunInput {
|
|
91
100
|
collaborationId: string;
|
|
101
|
+
workspaceId?: string;
|
|
92
102
|
phase?: MultiFrontierPhase;
|
|
93
103
|
participants: MultiFrontierParticipantState[];
|
|
94
104
|
approval?: MultiFrontierApproval;
|
|
95
105
|
checkpointIds?: string[];
|
|
106
|
+
autoContinueAfterAgreement?: boolean;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
export interface RecoverMultiFrontierRunInput {
|
|
@@ -114,6 +125,88 @@ export interface PersistedMultiFrontierParticipantEvent extends MultiFrontierPar
|
|
|
114
125
|
status?: MultiFrontierParticipantStatus;
|
|
115
126
|
}
|
|
116
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
|
+
|
|
117
210
|
export interface AppendMultiFrontierParticipantEventInput extends MultiFrontierParticipantEvent {
|
|
118
211
|
id: string;
|
|
119
212
|
collaborationId: string;
|
|
@@ -126,6 +219,14 @@ export interface MultiFrontierStoredRun extends MultiFrontierRunState {
|
|
|
126
219
|
updatedAt: string;
|
|
127
220
|
}
|
|
128
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
|
+
|
|
129
230
|
export type AppendMultiFrontierParticipantEventResult =
|
|
130
231
|
| {
|
|
131
232
|
accepted: true;
|
|
@@ -144,12 +245,44 @@ export type AppendMultiFrontierParticipantEventResult =
|
|
|
144
245
|
run: MultiFrontierStoredRun | null;
|
|
145
246
|
};
|
|
146
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
|
+
|
|
147
266
|
const TERMINAL_PHASES = new Set<MultiFrontierPhase>([
|
|
148
267
|
"completed",
|
|
149
268
|
"failed",
|
|
150
269
|
"canceled",
|
|
151
270
|
]);
|
|
152
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
|
+
};
|
|
153
286
|
|
|
154
287
|
/** All multi-frontier state stays alongside the existing local Code store. */
|
|
155
288
|
export function multiFrontierRunsStoreRoot(): string {
|
|
@@ -164,10 +297,17 @@ export function multiFrontierParticipantEventsDir(): string {
|
|
|
164
297
|
return path.join(multiFrontierRunsStoreRoot(), "events");
|
|
165
298
|
}
|
|
166
299
|
|
|
300
|
+
export function multiFrontierArtifactsDir(): string {
|
|
301
|
+
return path.join(multiFrontierRunsStoreRoot(), "artifacts");
|
|
302
|
+
}
|
|
303
|
+
|
|
167
304
|
export function createMultiFrontierRun(
|
|
168
305
|
input: CreateMultiFrontierRunInput,
|
|
169
306
|
): MultiFrontierStoredRun {
|
|
170
307
|
assertSafeId(input.collaborationId, "collaboration id");
|
|
308
|
+
if (input.workspaceId !== undefined) {
|
|
309
|
+
assertSafeId(input.workspaceId, "workspace id");
|
|
310
|
+
}
|
|
171
311
|
assertParticipants(input.participants);
|
|
172
312
|
if (getMultiFrontierRun(input.collaborationId)) {
|
|
173
313
|
throw new Error(
|
|
@@ -178,6 +318,7 @@ export function createMultiFrontierRun(
|
|
|
178
318
|
const state: MultiFrontierStoredRun = {
|
|
179
319
|
schemaVersion: 1,
|
|
180
320
|
collaborationId: input.collaborationId,
|
|
321
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
181
322
|
phase: input.phase ?? "proposing",
|
|
182
323
|
participants: input.participants.map((participant) => ({
|
|
183
324
|
...participant,
|
|
@@ -191,6 +332,7 @@ export function createMultiFrontierRun(
|
|
|
191
332
|
round: 1,
|
|
192
333
|
proposalIds: [],
|
|
193
334
|
reviewIds: [],
|
|
335
|
+
autoContinueAfterAgreement: input.autoContinueAfterAgreement ?? false,
|
|
194
336
|
createdAt: now,
|
|
195
337
|
updatedAt: now,
|
|
196
338
|
};
|
|
@@ -235,6 +377,7 @@ export function recoverMultiFrontierRun(
|
|
|
235
377
|
phase: "paused",
|
|
236
378
|
participants: state.participants.map((participant) => ({
|
|
237
379
|
...participant,
|
|
380
|
+
role: "watchdog",
|
|
238
381
|
permission: "read_only",
|
|
239
382
|
status: participant.status === "running" ? "waiting" : participant.status,
|
|
240
383
|
})),
|
|
@@ -253,11 +396,86 @@ export function recoverStoredMultiFrontierRun(
|
|
|
253
396
|
collaborationId: string,
|
|
254
397
|
input: RecoverMultiFrontierRunInput,
|
|
255
398
|
): MultiFrontierStoredRun | null {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
});
|
|
261
479
|
}
|
|
262
480
|
|
|
263
481
|
/**
|
|
@@ -376,10 +594,9 @@ export function activateStoredMultiFrontierDriver(
|
|
|
376
594
|
now = new Date().toISOString(),
|
|
377
595
|
): MultiFrontierStoredRun | null {
|
|
378
596
|
assertTimestamp(now, "activation time");
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return activated ? writeNextStoredRun(current, activated, now) : null;
|
|
597
|
+
return mutateStoredMultiFrontierRun(collaborationId, now, (current) =>
|
|
598
|
+
activateMultiFrontierDriver(current, participantId),
|
|
599
|
+
);
|
|
383
600
|
}
|
|
384
601
|
|
|
385
602
|
/** Persist an explicit driver reactivation after the human-approved resume. */
|
|
@@ -389,10 +606,9 @@ export function reactivateStoredMultiFrontierDriver(
|
|
|
389
606
|
now = new Date().toISOString(),
|
|
390
607
|
): MultiFrontierStoredRun | null {
|
|
391
608
|
assertTimestamp(now, "reactivation time");
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return reactivated ? writeNextStoredRun(current, reactivated, now) : null;
|
|
609
|
+
return mutateStoredMultiFrontierRun(collaborationId, now, (current) =>
|
|
610
|
+
reactivateMultiFrontierDriver(current, participantId),
|
|
611
|
+
);
|
|
396
612
|
}
|
|
397
613
|
|
|
398
614
|
/**
|
|
@@ -409,67 +625,190 @@ export function appendMultiFrontierParticipantEvent(
|
|
|
409
625
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
410
626
|
assertTimestamp(createdAt, "event time");
|
|
411
627
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
};
|
|
420
645
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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(
|
|
430
657
|
(candidate) => candidate.participantId === input.participantId,
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
441
|
-
if (
|
|
442
|
-
input.status !== undefined &&
|
|
443
|
-
(participant?.status === "completed" || participant?.status === "failed") &&
|
|
444
|
-
input.status !== participant.status
|
|
445
|
-
) {
|
|
446
|
-
return { accepted: false, reason: "terminal-participant", run: current };
|
|
447
|
-
}
|
|
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
|
+
}
|
|
448
667
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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,
|
|
465
719
|
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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),
|
|
471
794
|
);
|
|
472
|
-
|
|
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));
|
|
473
812
|
}
|
|
474
813
|
|
|
475
814
|
export function listMultiFrontierParticipantEvents(
|
|
@@ -489,30 +828,46 @@ export function listMultiFrontierParticipantEvents(
|
|
|
489
828
|
);
|
|
490
829
|
}
|
|
491
830
|
|
|
492
|
-
function
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
831
|
+
export function getMultiFrontierParticipantEventRetention(
|
|
832
|
+
collaborationId: string,
|
|
833
|
+
): MultiFrontierParticipantEventRetention | null {
|
|
834
|
+
if (!SAFE_ID.test(collaborationId)) return null;
|
|
835
|
+
return readParticipantEventRetention(
|
|
836
|
+
multiFrontierParticipantEventRetentionPath(collaborationId),
|
|
497
837
|
);
|
|
498
838
|
}
|
|
499
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
|
+
|
|
500
861
|
function writeNewMultiFrontierRun(run: MultiFrontierStoredRun): void {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
fs.
|
|
504
|
-
multiFrontierRunPath(run.collaborationId),
|
|
505
|
-
`${JSON.stringify(run, null, 2)}\n`,
|
|
506
|
-
{ encoding: "utf-8", flag: "wx" },
|
|
507
|
-
);
|
|
508
|
-
} catch (error) {
|
|
509
|
-
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
862
|
+
const filePath = multiFrontierRunPath(run.collaborationId);
|
|
863
|
+
withFileLockSync(filePath, () => {
|
|
864
|
+
if (fs.existsSync(filePath)) {
|
|
510
865
|
throw new Error(
|
|
511
866
|
`Multi-frontier run already exists: ${run.collaborationId}`,
|
|
512
867
|
);
|
|
513
868
|
}
|
|
514
|
-
|
|
515
|
-
}
|
|
869
|
+
writeJsonFileAtomically(filePath, run);
|
|
870
|
+
});
|
|
516
871
|
}
|
|
517
872
|
|
|
518
873
|
function writeNextStoredRun(
|
|
@@ -532,13 +887,90 @@ function writeNextStoredRun(
|
|
|
532
887
|
function appendParticipantEvent(
|
|
533
888
|
event: PersistedMultiFrontierParticipantEvent,
|
|
534
889
|
): void {
|
|
535
|
-
|
|
536
|
-
fs.appendFileSync(
|
|
890
|
+
appendUniqueJsonLineAtomically(
|
|
537
891
|
multiFrontierParticipantEventsPath(event.collaborationId),
|
|
538
|
-
|
|
892
|
+
event,
|
|
893
|
+
readParticipantEventValue,
|
|
894
|
+
{ mode: 0o600 },
|
|
539
895
|
);
|
|
540
896
|
}
|
|
541
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
|
+
|
|
542
974
|
function multiFrontierRunPath(collaborationId: string): string {
|
|
543
975
|
return path.join(multiFrontierRunsDir(), `${collaborationId}.json`);
|
|
544
976
|
}
|
|
@@ -550,46 +982,122 @@ function multiFrontierParticipantEventsPath(collaborationId: string): string {
|
|
|
550
982
|
);
|
|
551
983
|
}
|
|
552
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
|
+
|
|
553
1008
|
function readStoredRun(filePath: string): MultiFrontierStoredRun | null {
|
|
554
1009
|
try {
|
|
555
1010
|
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
run.schemaVersion !== 1 ||
|
|
560
|
-
typeof run.collaborationId !== "string" ||
|
|
561
|
-
!SAFE_ID.test(run.collaborationId) ||
|
|
562
|
-
!isPhase(run.phase) ||
|
|
563
|
-
!isStoredRunParticipantsAndDriverValid(run.participants, run.driver) ||
|
|
564
|
-
!isApproval(run.approval) ||
|
|
565
|
-
!Array.isArray(run.checkpointIds) ||
|
|
566
|
-
!run.checkpointIds.every(
|
|
567
|
-
(checkpointId) => typeof checkpointId === "string",
|
|
568
|
-
) ||
|
|
569
|
-
typeof run.round !== "number" ||
|
|
570
|
-
!Number.isInteger(run.round) ||
|
|
571
|
-
run.round < 1 ||
|
|
572
|
-
!isStringArray(run.proposalIds) ||
|
|
573
|
-
!isStringArray(run.reviewIds) ||
|
|
574
|
-
(run.recovery !== undefined && !isRecovery(run.recovery)) ||
|
|
575
|
-
typeof run.createdAt !== "string" ||
|
|
576
|
-
Number.isNaN(Date.parse(run.createdAt)) ||
|
|
577
|
-
typeof run.updatedAt !== "string" ||
|
|
578
|
-
Number.isNaN(Date.parse(run.updatedAt))
|
|
579
|
-
) {
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
return run as MultiFrontierStoredRun;
|
|
1011
|
+
const normalized = normalizeStoredRunDefaults(raw);
|
|
1012
|
+
return isCompleteMultiFrontierStoredRun(normalized) ? normalized : null;
|
|
583
1013
|
} catch {
|
|
584
1014
|
return null;
|
|
585
1015
|
}
|
|
586
1016
|
}
|
|
587
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
|
+
|
|
588
1087
|
function readParticipantEvent(
|
|
589
1088
|
line: string,
|
|
590
1089
|
): PersistedMultiFrontierParticipantEvent | null {
|
|
591
1090
|
try {
|
|
592
|
-
|
|
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 {
|
|
593
1101
|
if (!raw || typeof raw !== "object") return null;
|
|
594
1102
|
const event = raw as Partial<PersistedMultiFrontierParticipantEvent>;
|
|
595
1103
|
if (
|
|
@@ -612,6 +1120,627 @@ function readParticipantEvent(
|
|
|
612
1120
|
}
|
|
613
1121
|
}
|
|
614
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
|
+
|
|
615
1744
|
function assertParticipants(
|
|
616
1745
|
participants: MultiFrontierParticipantState[],
|
|
617
1746
|
): void {
|
|
@@ -821,21 +1950,3 @@ function assertTimestamp(value: string, label: string): void {
|
|
|
821
1950
|
throw new Error(`Invalid multi-frontier ${label}.`);
|
|
822
1951
|
}
|
|
823
1952
|
}
|
|
824
|
-
|
|
825
|
-
function writeFileAtomically(filePath: string, contents: string): void {
|
|
826
|
-
const directory = path.dirname(filePath);
|
|
827
|
-
const temporaryPath = path.join(
|
|
828
|
-
directory,
|
|
829
|
-
`.${path.basename(filePath)}.tmp-${process.pid}-${crypto.randomUUID()}`,
|
|
830
|
-
);
|
|
831
|
-
try {
|
|
832
|
-
fs.writeFileSync(temporaryPath, contents, {
|
|
833
|
-
encoding: "utf-8",
|
|
834
|
-
flag: "wx",
|
|
835
|
-
});
|
|
836
|
-
fs.renameSync(temporaryPath, filePath);
|
|
837
|
-
} catch (error) {
|
|
838
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
839
|
-
throw error;
|
|
840
|
-
}
|
|
841
|
-
}
|