mulmoclaude 0.1.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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
// The daily pass: walk chat/*.jsonl, find sessions changed since
|
|
2
|
+
// the last run, bucket events by local-date, call the archivist
|
|
3
|
+
// once per affected day, and apply its output (write daily/*.md,
|
|
4
|
+
// create/append/rewrite topics/*.md).
|
|
5
|
+
//
|
|
6
|
+
// This file is the only one in the journal module that combines
|
|
7
|
+
// filesystem side-effects with LLM calls. Pure bits (event parsing,
|
|
8
|
+
// bucketing) are factored into small exported helpers so tests can
|
|
9
|
+
// exercise them without touching disk.
|
|
10
|
+
|
|
11
|
+
import fsp from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
14
|
+
import { WORKSPACE_DIRS } from "../paths.js";
|
|
15
|
+
import { writeDailySummary, readDailySummary, readTopicFile, writeTopicFile, appendOrCreateTopic, readAllTopicFiles } from "../../utils/files/journal-io.js";
|
|
16
|
+
import { readSessionMeta as readSessionMetaIO, readSessionJsonl as readSessionJsonlIO } from "../../utils/files/session-io.js";
|
|
17
|
+
import { statUnder } from "../../utils/files/workspace-io.js";
|
|
18
|
+
import {
|
|
19
|
+
type Summarize,
|
|
20
|
+
type SessionExcerpt,
|
|
21
|
+
type SessionEventExcerpt,
|
|
22
|
+
type ExistingTopicSnapshot,
|
|
23
|
+
type DailyArchivistInput,
|
|
24
|
+
type DailyArchivistOutput,
|
|
25
|
+
type TopicUpdate,
|
|
26
|
+
DAILY_SYSTEM_PROMPT,
|
|
27
|
+
buildDailyUserPrompt,
|
|
28
|
+
extractJsonObject,
|
|
29
|
+
isDailyArchivistOutput,
|
|
30
|
+
ClaudeCliNotFoundError,
|
|
31
|
+
} from "./archivist.js";
|
|
32
|
+
import { toIsoDate, slugify } from "./paths.js";
|
|
33
|
+
import { findDirtySessions, applyProcessed, type SessionFileMeta } from "./diff.js";
|
|
34
|
+
import { rewriteWorkspaceLinks } from "./linkRewrite.js";
|
|
35
|
+
import { writeState, type JournalState } from "./state.js";
|
|
36
|
+
import { log } from "../../system/logger/index.js";
|
|
37
|
+
import { EVENT_TYPES } from "../../../src/types/events.js";
|
|
38
|
+
import { extractAndAppendMemory } from "./memoryExtractor.js";
|
|
39
|
+
import { isRecord } from "../../utils/types.js";
|
|
40
|
+
|
|
41
|
+
// --- Constants ------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// Per-event content is truncated before handing to the archivist so
|
|
44
|
+
// an accidentally huge tool result (e.g. base64 image data) doesn't
|
|
45
|
+
// blow past the CLI's context window.
|
|
46
|
+
const MAX_EVENT_CONTENT_CHARS = 600;
|
|
47
|
+
|
|
48
|
+
// Hard cap on events per session included in the prompt. Sessions
|
|
49
|
+
// with thousands of events get their head kept — the archivist can
|
|
50
|
+
// generally get the gist from the opening.
|
|
51
|
+
const MAX_EVENTS_PER_SESSION = 80;
|
|
52
|
+
|
|
53
|
+
// --- Public entry ---------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export interface DailyPassDeps {
|
|
56
|
+
workspaceRoot?: string;
|
|
57
|
+
summarize: Summarize;
|
|
58
|
+
// Active session ids to skip (mid-write). Caller passes the
|
|
59
|
+
// live session registry to avoid ingesting jsonl files that the
|
|
60
|
+
// agent is still appending to.
|
|
61
|
+
activeSessionIds: ReadonlySet<string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DailyPassResult {
|
|
65
|
+
daysTouched: string[]; // YYYY-MM-DD values actually written
|
|
66
|
+
sessionsIngested: string[];
|
|
67
|
+
topicsCreated: string[];
|
|
68
|
+
topicsUpdated: string[];
|
|
69
|
+
skipped: Array<{ date: string; reason: string }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runDailyPass(state: JournalState, deps: DailyPassDeps): Promise<{ nextState: JournalState; result: DailyPassResult }> {
|
|
73
|
+
const workspaceRoot = deps.workspaceRoot ?? defaultWorkspacePath;
|
|
74
|
+
const chatDir = path.join(workspaceRoot, WORKSPACE_DIRS.chat);
|
|
75
|
+
const result: DailyPassResult = {
|
|
76
|
+
daysTouched: [],
|
|
77
|
+
sessionsIngested: [],
|
|
78
|
+
topicsCreated: [],
|
|
79
|
+
topicsUpdated: [],
|
|
80
|
+
skipped: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// --- Phase 1: figure out what work there is to do ------------------
|
|
84
|
+
const eligible = (await listSessionMetas(chatDir)).filter((m) => !deps.activeSessionIds.has(m.id));
|
|
85
|
+
const { dirty } = findDirtySessions(eligible, state.processedSessions);
|
|
86
|
+
if (dirty.length === 0) return { nextState: { ...state }, result };
|
|
87
|
+
|
|
88
|
+
const perSessionExcerpts = await loadDirtySessionExcerpts(chatDir, dirty, workspaceRoot);
|
|
89
|
+
const { dayBuckets, sessionToDays } = buildDayBuckets(perSessionExcerpts);
|
|
90
|
+
|
|
91
|
+
// Note: we intentionally do NOT early-return when `dayBuckets` is
|
|
92
|
+
// empty. Letting the pipeline fall through preserves the pre-
|
|
93
|
+
// refactor behaviour for the edge case where every dirty session
|
|
94
|
+
// produces zero excerpts (all malformed, or all metadata/tool-only
|
|
95
|
+
// with no text turns): `readAllTopics` still fires, and the
|
|
96
|
+
// returned `nextState.knownTopics` is still normalized / sorted
|
|
97
|
+
// from the existing state. The empty `orderedDays` loop then
|
|
98
|
+
// iterates zero times and we fall through to `return { nextState,
|
|
99
|
+
// result }`.
|
|
100
|
+
|
|
101
|
+
// --- Phase 2: set up per-pass state --------------------------------
|
|
102
|
+
const existingTopics = await readAllTopics(workspaceRoot);
|
|
103
|
+
const newTopicsSeen = new Set<string>(state.knownTopics);
|
|
104
|
+
// `nextState` is rebuilt through the day loop and persisted after
|
|
105
|
+
// each successful day via writeState (atomic tmp+rename). We do
|
|
106
|
+
// NOT bump lastDailyRunAt here — that's the outer runner's job
|
|
107
|
+
// after the whole pass (including optimization) finishes, so
|
|
108
|
+
// partial progress doesn't look like a complete pass.
|
|
109
|
+
let nextState: JournalState = {
|
|
110
|
+
...state,
|
|
111
|
+
knownTopics: [...newTopicsSeen].sort(),
|
|
112
|
+
};
|
|
113
|
+
const dirtyMetaById = new Map(eligible.map((m) => [m.id, m]));
|
|
114
|
+
// Process days in chronological order so topic state accumulates
|
|
115
|
+
// naturally: an earlier day's update is visible to the next day.
|
|
116
|
+
const orderedDays = [...dayBuckets.keys()].sort();
|
|
117
|
+
|
|
118
|
+
// --- Phase 3: process each day -------------------------------------
|
|
119
|
+
for (const date of orderedDays) {
|
|
120
|
+
const dayResult = await processDayAndAdvance({
|
|
121
|
+
workspaceRoot,
|
|
122
|
+
date,
|
|
123
|
+
dayBuckets,
|
|
124
|
+
existingTopics,
|
|
125
|
+
summarize: deps.summarize,
|
|
126
|
+
sessionToDays,
|
|
127
|
+
dirtyMetaById,
|
|
128
|
+
newTopicsSeen,
|
|
129
|
+
nextState,
|
|
130
|
+
});
|
|
131
|
+
if (dayResult.kind === "skipped") {
|
|
132
|
+
result.skipped.push({ date, reason: dayResult.reason });
|
|
133
|
+
} else {
|
|
134
|
+
result.daysTouched.push(date);
|
|
135
|
+
result.topicsCreated.push(...dayResult.topicsCreated);
|
|
136
|
+
result.topicsUpdated.push(...dayResult.topicsUpdated);
|
|
137
|
+
result.sessionsIngested.push(...dayResult.sessionsIngested);
|
|
138
|
+
}
|
|
139
|
+
nextState = dayResult.nextState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Phase 4: memory extraction ------------------------------------
|
|
143
|
+
await maybeExtractMemory(perSessionExcerpts, workspaceRoot, deps);
|
|
144
|
+
|
|
145
|
+
return { nextState, result };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Phase 3 helper: single-day processing + state advance -----------
|
|
149
|
+
// Extracted from the Phase 3 for-loop to keep runDailyPass under
|
|
150
|
+
// the sonarjs/cognitive-complexity threshold.
|
|
151
|
+
|
|
152
|
+
interface ProcessDayInput {
|
|
153
|
+
workspaceRoot: string;
|
|
154
|
+
date: string;
|
|
155
|
+
dayBuckets: ReadonlyMap<string, SessionExcerpt[]>;
|
|
156
|
+
existingTopics: ExistingTopicSnapshot[];
|
|
157
|
+
summarize: Summarize;
|
|
158
|
+
sessionToDays: Map<string, Set<string>>;
|
|
159
|
+
dirtyMetaById: ReadonlyMap<string, SessionFileMeta>;
|
|
160
|
+
newTopicsSeen: Set<string>;
|
|
161
|
+
nextState: JournalState;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type ProcessDayOutput =
|
|
165
|
+
| {
|
|
166
|
+
kind: "skipped";
|
|
167
|
+
reason: string;
|
|
168
|
+
nextState: JournalState;
|
|
169
|
+
}
|
|
170
|
+
| {
|
|
171
|
+
kind: "processed";
|
|
172
|
+
topicsCreated: string[];
|
|
173
|
+
topicsUpdated: string[];
|
|
174
|
+
sessionsIngested: string[];
|
|
175
|
+
nextState: JournalState;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
async function processDayAndAdvance(input: ProcessDayInput): Promise<ProcessDayOutput> {
|
|
179
|
+
const excerpts = input.dayBuckets.get(input.date) ?? [];
|
|
180
|
+
const dayOutcome = await processOneDay(input.workspaceRoot, input.date, excerpts, input.existingTopics, input.summarize);
|
|
181
|
+
if (dayOutcome.kind === "skipped") {
|
|
182
|
+
return {
|
|
183
|
+
kind: "skipped",
|
|
184
|
+
reason: dayOutcome.reason,
|
|
185
|
+
nextState: input.nextState,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const slug of dayOutcome.topicsTouched) {
|
|
190
|
+
input.newTopicsSeen.add(slug);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const justCompleted = computeJustCompletedSessions(input.date, excerpts, input.sessionToDays, input.dirtyMetaById);
|
|
194
|
+
const sessionsIngested = justCompleted.map((m) => m.id);
|
|
195
|
+
const nextState = advanceJournalState(input.nextState, justCompleted, input.newTopicsSeen);
|
|
196
|
+
await persistStateAfterDay(input.workspaceRoot, nextState, input.date);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
kind: "processed",
|
|
200
|
+
topicsCreated: dayOutcome.topicsCreated,
|
|
201
|
+
topicsUpdated: dayOutcome.topicsUpdated,
|
|
202
|
+
sessionsIngested,
|
|
203
|
+
nextState,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Phase 4 helper: memory extraction -------------------------------
|
|
208
|
+
// Scan dirty-session excerpts for durable user facts and append new
|
|
209
|
+
// ones to memory.md. Fire-and-forget: if extraction fails the daily
|
|
210
|
+
// summaries are already written, so the pass is still useful.
|
|
211
|
+
|
|
212
|
+
async function maybeExtractMemory(
|
|
213
|
+
perSessionExcerpts: ReadonlyMap<string, ReadonlyMap<string, SessionExcerpt>>,
|
|
214
|
+
workspaceRoot: string,
|
|
215
|
+
deps: DailyPassDeps,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
if (perSessionExcerpts.size === 0) return;
|
|
218
|
+
const excerptLines: string[] = [];
|
|
219
|
+
for (const [, byDate] of perSessionExcerpts) {
|
|
220
|
+
for (const [, excerpt] of byDate) {
|
|
221
|
+
const userLines = excerpt.events.filter((e: SessionEventExcerpt) => e.source === "user").map((e: SessionEventExcerpt) => `[user] ${e.content}`);
|
|
222
|
+
if (userLines.length > 0) excerptLines.push(userLines.join("\n"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await extractAndAppendMemory({
|
|
227
|
+
workspaceRoot,
|
|
228
|
+
excerpts: excerptLines.join("\n---\n"),
|
|
229
|
+
summarize: deps.summarize,
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
log.warn("daily-pass", "memory extraction failed (non-fatal)", {
|
|
233
|
+
error: String(err),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Phase 3 helper: per-day side-effecting pipeline ----------------
|
|
239
|
+
|
|
240
|
+
// Discriminated return so `runDailyPass` can branch on outcome
|
|
241
|
+
// without digging into null or throwing.
|
|
242
|
+
export type DayOutcome =
|
|
243
|
+
| { kind: "skipped"; reason: string }
|
|
244
|
+
| {
|
|
245
|
+
kind: "processed";
|
|
246
|
+
topicsCreated: string[];
|
|
247
|
+
topicsUpdated: string[];
|
|
248
|
+
// Union of created + updated — handed back so the caller
|
|
249
|
+
// can keep `newTopicsSeen` in sync without recomputing.
|
|
250
|
+
topicsTouched: string[];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Run the archivist for one day and apply its output (daily
|
|
254
|
+
// summary + topic updates). All filesystem writes land here so
|
|
255
|
+
// `runDailyPass` stays branching-lean.
|
|
256
|
+
async function processOneDay(
|
|
257
|
+
workspaceRoot: string,
|
|
258
|
+
date: string,
|
|
259
|
+
excerpts: SessionExcerpt[],
|
|
260
|
+
existingTopics: ExistingTopicSnapshot[],
|
|
261
|
+
summarize: Summarize,
|
|
262
|
+
): Promise<DayOutcome> {
|
|
263
|
+
const existingDaily = await readDailySummary(date, workspaceRoot);
|
|
264
|
+
const input: DailyArchivistInput = {
|
|
265
|
+
date,
|
|
266
|
+
existingDailySummary: existingDaily,
|
|
267
|
+
existingTopicSummaries: existingTopics,
|
|
268
|
+
sessionExcerpts: excerpts,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const rawOutput = await callSummarizeForDay(date, input, summarize);
|
|
272
|
+
if (rawOutput === null) {
|
|
273
|
+
return { kind: "skipped", reason: "summarize failed" };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const parsed = parseArchivistOutput(rawOutput);
|
|
277
|
+
if (parsed === null) {
|
|
278
|
+
log.warn("journal", "archivist returned unusable JSON, skipping", {
|
|
279
|
+
date,
|
|
280
|
+
});
|
|
281
|
+
return { kind: "skipped", reason: "unusable archivist JSON" };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await writeDailySummaryForDate(workspaceRoot, date, parsed.dailySummaryMarkdown);
|
|
285
|
+
|
|
286
|
+
const topicOutcome = await processTopicUpdatesForDay(workspaceRoot, parsed.topicUpdates, existingTopics);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
kind: "processed",
|
|
290
|
+
topicsCreated: topicOutcome.created,
|
|
291
|
+
topicsUpdated: topicOutcome.updated,
|
|
292
|
+
topicsTouched: [...topicOutcome.created, ...topicOutcome.updated],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Call the archivist summarizer and narrow its failure modes.
|
|
297
|
+
// Returns null on recoverable failures (logged + skipped), throws
|
|
298
|
+
// only for `ClaudeCliNotFoundError` which the outer runner uses to
|
|
299
|
+
// disable the whole journal feature for the process lifetime.
|
|
300
|
+
async function callSummarizeForDay(date: string, input: DailyArchivistInput, summarize: Summarize): Promise<string | null> {
|
|
301
|
+
try {
|
|
302
|
+
return await summarize(DAILY_SYSTEM_PROMPT, buildDailyUserPrompt(input));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (err instanceof ClaudeCliNotFoundError) throw err;
|
|
305
|
+
log.warn("journal", "summarize failed, skipping day", {
|
|
306
|
+
date,
|
|
307
|
+
error: String(err),
|
|
308
|
+
});
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Side-effecting wrapper: rewrite workspace-absolute links in the
|
|
314
|
+
// archivist output relative to the daily summary's own location,
|
|
315
|
+
// then write the file to disk. Factored out so the main loop's
|
|
316
|
+
// body no longer contains path-math and I/O intermixed.
|
|
317
|
+
async function writeDailySummaryForDate(workspaceRoot: string, date: string, rawMarkdown: string): Promise<void> {
|
|
318
|
+
// Rewrite any /workspace-absolute links in the archivist's output
|
|
319
|
+
// into true-relative links from the daily summary's location
|
|
320
|
+
// before writing to disk. Same treatment below for topic files.
|
|
321
|
+
const [yearPart, monthPart, dayPart] = date.split("-");
|
|
322
|
+
const dailyFileWsPath = path.posix.join(WORKSPACE_DIRS.summaries, "daily", yearPart, monthPart, `${dayPart}.md`);
|
|
323
|
+
const content = rewriteWorkspaceLinks(dailyFileWsPath, rawMarkdown);
|
|
324
|
+
await writeDailySummary(date, content, workspaceRoot);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Apply every topic update the archivist asked for, keeping the
|
|
328
|
+
// in-memory `existingTopics` snapshot in sync so the next day in
|
|
329
|
+
// this same pass sees fresh content. Mutates `existingTopics`.
|
|
330
|
+
//
|
|
331
|
+
// Per-update failures (EACCES, EIO, etc. surfaced by appendOrCreate)
|
|
332
|
+
// are logged and skipped so a single broken topic file doesn't kill
|
|
333
|
+
// the whole pass after days of progress have already been committed.
|
|
334
|
+
async function processTopicUpdatesForDay(
|
|
335
|
+
workspaceRoot: string,
|
|
336
|
+
updates: readonly TopicUpdate[],
|
|
337
|
+
existingTopics: ExistingTopicSnapshot[],
|
|
338
|
+
): Promise<{ created: string[]; updated: string[] }> {
|
|
339
|
+
const created: string[] = [];
|
|
340
|
+
const updated: string[] = [];
|
|
341
|
+
for (const update of updates) {
|
|
342
|
+
const normalized = normalizeTopicAction(update, existingTopics);
|
|
343
|
+
try {
|
|
344
|
+
const outcome = await applyTopicUpdate(workspaceRoot, normalized);
|
|
345
|
+
if (outcome === "created") created.push(normalized.slug);
|
|
346
|
+
else if (outcome === "updated") updated.push(normalized.slug);
|
|
347
|
+
await refreshTopicSnapshot(workspaceRoot, normalized.slug, existingTopics);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
log.warn("journal", "failed to apply topic update", {
|
|
350
|
+
slug: normalized.slug,
|
|
351
|
+
error: String(err),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { created, updated };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Re-read the topic file fresh and upsert its snapshot into the
|
|
359
|
+
// in-memory `existingTopics` list so the next day's archivist
|
|
360
|
+
// call sees the latest content.
|
|
361
|
+
async function refreshTopicSnapshot(workspaceRoot: string, slug: string, existingTopics: ExistingTopicSnapshot[]): Promise<void> {
|
|
362
|
+
const newBody = await readTopicFile(slug, workspaceRoot);
|
|
363
|
+
if (newBody === null) return;
|
|
364
|
+
const snapshot: ExistingTopicSnapshot = { slug, content: newBody };
|
|
365
|
+
const idx = existingTopics.findIndex((t) => t.slug === slug);
|
|
366
|
+
if (idx === -1) existingTopics.push(snapshot);
|
|
367
|
+
else existingTopics[idx] = snapshot;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Persist the in-progress journal state after each day so a
|
|
371
|
+
// mid-pass crash only costs the work written after the last
|
|
372
|
+
// checkpoint. Write failures are logged but don't fail the pass —
|
|
373
|
+
// the day's markdown is already on disk and the next run will
|
|
374
|
+
// catch up.
|
|
375
|
+
async function persistStateAfterDay(workspaceRoot: string, state: JournalState, date: string): Promise<void> {
|
|
376
|
+
try {
|
|
377
|
+
await writeState(workspaceRoot, state);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
log.warn("journal", "failed to persist state after day", {
|
|
380
|
+
date,
|
|
381
|
+
error: String(err),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- Pure helpers (exported for unit tests) ------------------------
|
|
387
|
+
|
|
388
|
+
// Bucket every session's per-date excerpts into a `dayBuckets`
|
|
389
|
+
// map and a `sessionToDays` tracking map in one pass. Inputs are
|
|
390
|
+
// the per-session excerpts loaded by `loadDirtySessionExcerpts`;
|
|
391
|
+
// outputs are plain Maps the day loop can consume directly.
|
|
392
|
+
//
|
|
393
|
+
// `sessionToDays` is used later by `computeJustCompletedSessions`
|
|
394
|
+
// to mark a session fully processed only after its last day has
|
|
395
|
+
// been written. That's why both Maps are built here together —
|
|
396
|
+
// they're two views of the same input and staying in sync matters.
|
|
397
|
+
export interface DayBucketsPlan {
|
|
398
|
+
dayBuckets: Map<string, SessionExcerpt[]>;
|
|
399
|
+
sessionToDays: Map<string, Set<string>>;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function buildDayBuckets(perSessionExcerpts: ReadonlyMap<string, ReadonlyMap<string, SessionExcerpt>>): DayBucketsPlan {
|
|
403
|
+
const dayBuckets = new Map<string, SessionExcerpt[]>();
|
|
404
|
+
const sessionToDays = new Map<string, Set<string>>();
|
|
405
|
+
for (const [sessionId, byDate] of perSessionExcerpts) {
|
|
406
|
+
for (const [date, excerpt] of byDate) {
|
|
407
|
+
const bucket = dayBuckets.get(date);
|
|
408
|
+
if (bucket) bucket.push(excerpt);
|
|
409
|
+
else dayBuckets.set(date, [excerpt]);
|
|
410
|
+
|
|
411
|
+
let days = sessionToDays.get(sessionId);
|
|
412
|
+
if (!days) {
|
|
413
|
+
days = new Set<string>();
|
|
414
|
+
sessionToDays.set(sessionId, days);
|
|
415
|
+
}
|
|
416
|
+
days.add(date);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { dayBuckets, sessionToDays };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Apply the append-to-missing → create guard and canonicalise
|
|
423
|
+
// the slug. The archivist occasionally asks to "append" to a
|
|
424
|
+
// brand-new topic; silently promoting that to "create" removes a
|
|
425
|
+
// whole class of LLM mistakes without needing a schema rejection.
|
|
426
|
+
// Also rewrites any workspace-absolute links in the body relative
|
|
427
|
+
// to the target topic file's location.
|
|
428
|
+
export function normalizeTopicAction(update: TopicUpdate, existingTopics: readonly ExistingTopicSnapshot[]): TopicUpdate {
|
|
429
|
+
const canonicalSlug = slugify(update.slug);
|
|
430
|
+
const exists = existingTopics.some((t) => t.slug === canonicalSlug);
|
|
431
|
+
const topicFileWsPath = path.posix.join(WORKSPACE_DIRS.summaries, "topics", `${canonicalSlug}.md`);
|
|
432
|
+
return {
|
|
433
|
+
slug: canonicalSlug,
|
|
434
|
+
action: !exists && update.action === "append" ? "create" : update.action,
|
|
435
|
+
content: rewriteWorkspaceLinks(topicFileWsPath, update.content),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Parse an archivist raw output string into a validated
|
|
440
|
+
// DailyArchivistOutput. Returns null when the JSON envelope is
|
|
441
|
+
// missing or the shape doesn't match, so callers can treat it
|
|
442
|
+
// as a skip reason without needing a separate `isValid` check.
|
|
443
|
+
// Pure — combines `extractJsonObject` + `isDailyArchivistOutput`
|
|
444
|
+
// behind a single gate.
|
|
445
|
+
export function parseArchivistOutput(rawOutput: string): DailyArchivistOutput | null {
|
|
446
|
+
const parsed = extractJsonObject(rawOutput);
|
|
447
|
+
if (!isDailyArchivistOutput(parsed)) return null;
|
|
448
|
+
return parsed;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Decide which sessions have just completed their last pending
|
|
452
|
+
// day, mutating `sessionToDays` to drop `date` from every entry
|
|
453
|
+
// that touches it. Returns the `SessionFileMeta` records for the
|
|
454
|
+
// freshly-completed sessions so the caller can feed them into
|
|
455
|
+
// `applyProcessed`.
|
|
456
|
+
//
|
|
457
|
+
// A session is "complete" when its pending-days set, *after*
|
|
458
|
+
// removing the current date, is empty. Sessions not in
|
|
459
|
+
// `sessionToDays` (or not in `dirtyMetaById`) are silently
|
|
460
|
+
// skipped — defensive against unexpected inputs, and the same
|
|
461
|
+
// shape as the pre-refactor inline code.
|
|
462
|
+
export function computeJustCompletedSessions(
|
|
463
|
+
date: string,
|
|
464
|
+
excerpts: readonly SessionExcerpt[],
|
|
465
|
+
sessionToDays: Map<string, Set<string>>,
|
|
466
|
+
dirtyMetaById: ReadonlyMap<string, SessionFileMeta>,
|
|
467
|
+
): SessionFileMeta[] {
|
|
468
|
+
const justCompleted: SessionFileMeta[] = [];
|
|
469
|
+
for (const excerpt of excerpts) {
|
|
470
|
+
const pending = sessionToDays.get(excerpt.sessionId);
|
|
471
|
+
if (!pending) continue;
|
|
472
|
+
pending.delete(date);
|
|
473
|
+
if (pending.size === 0) {
|
|
474
|
+
sessionToDays.delete(excerpt.sessionId);
|
|
475
|
+
const meta = dirtyMetaById.get(excerpt.sessionId);
|
|
476
|
+
if (meta) justCompleted.push(meta);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return justCompleted;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Build the next JournalState from the previous one plus a batch
|
|
483
|
+
// of just-completed sessions and the current view of known
|
|
484
|
+
// topics. Tiny pure wrapper so the day loop has one place to
|
|
485
|
+
// advance state instead of five-line spread literals scattered
|
|
486
|
+
// through it.
|
|
487
|
+
export function advanceJournalState(prev: JournalState, justCompleted: readonly SessionFileMeta[], newTopicsSeen: ReadonlySet<string>): JournalState {
|
|
488
|
+
return {
|
|
489
|
+
...prev,
|
|
490
|
+
processedSessions: applyProcessed(prev.processedSessions, [...justCompleted]),
|
|
491
|
+
knownTopics: [...newTopicsSeen].sort(),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- Filesystem helpers ---------------------------------------------
|
|
496
|
+
|
|
497
|
+
// Load every dirty session's jsonl, bucket events by local-date,
|
|
498
|
+
// and return the whole collection as a Map<sessionId, Map<date,
|
|
499
|
+
// excerpt>>. Malformed sessions are logged and skipped so one
|
|
500
|
+
// bad jsonl can't crash the pass. Returned shape is exactly what
|
|
501
|
+
// `buildDayBuckets` wants as input.
|
|
502
|
+
async function loadDirtySessionExcerpts(chatDir: string, dirty: readonly string[], workspaceRoot: string): Promise<Map<string, Map<string, SessionExcerpt>>> {
|
|
503
|
+
const perSession = new Map<string, Map<string, SessionExcerpt>>();
|
|
504
|
+
for (const sessionId of dirty) {
|
|
505
|
+
try {
|
|
506
|
+
const excerpts = await loadSessionExcerptsByDate(chatDir, sessionId, workspaceRoot);
|
|
507
|
+
if (excerpts.size > 0) perSession.set(sessionId, excerpts);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
log.warn("journal", "failed to load session", {
|
|
510
|
+
sessionId,
|
|
511
|
+
error: String(err),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return perSession;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function listSessionMetas(chatDir: string): Promise<SessionFileMeta[]> {
|
|
519
|
+
let entries: string[];
|
|
520
|
+
try {
|
|
521
|
+
entries = await fsp.readdir(chatDir);
|
|
522
|
+
} catch {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
const out: SessionFileMeta[] = [];
|
|
526
|
+
for (const name of entries) {
|
|
527
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
528
|
+
const full = path.join(chatDir, name);
|
|
529
|
+
try {
|
|
530
|
+
const st = await fsp.stat(full);
|
|
531
|
+
out.push({
|
|
532
|
+
id: name.replace(/\.jsonl$/, ""),
|
|
533
|
+
mtimeMs: st.mtimeMs,
|
|
534
|
+
});
|
|
535
|
+
} catch {
|
|
536
|
+
// file vanished between readdir and stat — ignore
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function loadSessionExcerptsByDate(chatDir: string, sessionId: string, workspaceRoot: string): Promise<Map<string, SessionExcerpt>> {
|
|
543
|
+
const roleId = await readRoleIdFromMeta(sessionId, workspaceRoot);
|
|
544
|
+
const raw = await readSessionJsonlIO(sessionId, workspaceRoot);
|
|
545
|
+
if (!raw) return new Map();
|
|
546
|
+
|
|
547
|
+
const stat = await statUnder(workspaceRoot, path.posix.join(WORKSPACE_DIRS.chat, `${sessionId}.jsonl`));
|
|
548
|
+
const fallbackDate = toIsoDate(stat?.mtimeMs ?? Date.now());
|
|
549
|
+
|
|
550
|
+
const parsedEvents = parseJsonlEvents(raw, MAX_EVENTS_PER_SESSION);
|
|
551
|
+
return bucketParsedEvents(parsedEvents, sessionId, roleId, fallbackDate);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Walk a jsonl string and return at most `maxEvents` parsed events
|
|
555
|
+
// ready for bucketing. Skips blank lines, malformed JSON,
|
|
556
|
+
// metadata entries, and anything `parseEntry` rejects. Pure —
|
|
557
|
+
// exported so tests can exercise it with fabricated jsonl strings.
|
|
558
|
+
export function parseJsonlEvents(raw: string, maxEvents: number): ParsedEntry[] {
|
|
559
|
+
const events: ParsedEntry[] = [];
|
|
560
|
+
for (const line of raw.split("\n")) {
|
|
561
|
+
if (events.length >= maxEvents) break;
|
|
562
|
+
const entry = parseJsonlLine(line);
|
|
563
|
+
if (entry === null) continue;
|
|
564
|
+
if (isMetadataEntry(entry)) continue;
|
|
565
|
+
const parsed = parseEntry(entry);
|
|
566
|
+
if (parsed) events.push(parsed);
|
|
567
|
+
}
|
|
568
|
+
return events;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// JSON.parse one jsonl line, guarding against blank lines,
|
|
572
|
+
// malformed JSON, and any JSON value that isn't a plain object.
|
|
573
|
+
// `JSON.parse` will happily return `null`, arrays, strings,
|
|
574
|
+
// numbers, or booleans, none of which the downstream
|
|
575
|
+
// `parseEntry` / `entryToExcerpt` functions can consume — and
|
|
576
|
+
// `entry.type` on a `null` or primitive throws at runtime.
|
|
577
|
+
// Returning `null` here collapses every invalid shape into the
|
|
578
|
+
// same "skip this line" sentinel the caller already handles.
|
|
579
|
+
function parseJsonlLine(line: string): Record<string, unknown> | null {
|
|
580
|
+
if (!line.trim()) return null;
|
|
581
|
+
let parsed: unknown;
|
|
582
|
+
try {
|
|
583
|
+
parsed = JSON.parse(line);
|
|
584
|
+
} catch {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (!isRecord(parsed)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return parsed as Record<string, unknown>;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isMetadataEntry(entry: Record<string, unknown>): boolean {
|
|
594
|
+
return entry.type === EVENT_TYPES.sessionMeta || entry.type === EVENT_TYPES.claudeSessionId;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Collect parsed events into per-date buckets using `fallbackDate`
|
|
598
|
+
// for every event, since the legacy jsonl format has no per-event
|
|
599
|
+
// timestamps. Extracted so the I/O-free bucket-building can be
|
|
600
|
+
// reasoned about and unit-tested without a real jsonl file.
|
|
601
|
+
export function bucketParsedEvents(events: readonly ParsedEntry[], sessionId: string, roleId: string, fallbackDate: string): Map<string, SessionExcerpt> {
|
|
602
|
+
const buckets = new Map<string, SessionExcerpt>();
|
|
603
|
+
for (const parsed of events) {
|
|
604
|
+
let bucket = buckets.get(fallbackDate);
|
|
605
|
+
if (!bucket) {
|
|
606
|
+
bucket = { sessionId, roleId, events: [], artifactPaths: [] };
|
|
607
|
+
buckets.set(fallbackDate, bucket);
|
|
608
|
+
}
|
|
609
|
+
bucket.events.push(parsed.excerpt);
|
|
610
|
+
for (const p of parsed.artifactPaths) {
|
|
611
|
+
if (!bucket.artifactPaths.includes(p)) bucket.artifactPaths.push(p);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return buckets;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function readRoleIdFromMeta(sessionId: string, workspaceRoot: string): Promise<string> {
|
|
618
|
+
try {
|
|
619
|
+
const meta = await readSessionMetaIO(sessionId, workspaceRoot);
|
|
620
|
+
if (meta && typeof meta.roleId === "string") return meta.roleId;
|
|
621
|
+
} catch {
|
|
622
|
+
// ignore
|
|
623
|
+
}
|
|
624
|
+
return "unknown";
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Convert one jsonl entry into a flat excerpt the archivist can read,
|
|
628
|
+
// plus any workspace-relative artifact paths the entry references.
|
|
629
|
+
// Exported so tests can exercise it with fabricated entries.
|
|
630
|
+
export interface ParsedEntry {
|
|
631
|
+
excerpt: SessionEventExcerpt;
|
|
632
|
+
// 0+ workspace-relative artifact paths referenced by this entry.
|
|
633
|
+
// Used to build the ARTIFACTS REFERENCED prompt section.
|
|
634
|
+
artifactPaths: string[];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function parseEntry(entry: Record<string, unknown>): ParsedEntry | null {
|
|
638
|
+
const excerpt = entryToExcerpt(entry);
|
|
639
|
+
if (!excerpt) return null;
|
|
640
|
+
return {
|
|
641
|
+
excerpt,
|
|
642
|
+
artifactPaths: extractArtifactPaths(entry),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Legacy single-purpose form used by the existing unit tests.
|
|
647
|
+
// Prefer `parseEntry` for code that also wants artifact paths.
|
|
648
|
+
export function entryToExcerpt(entry: Record<string, unknown>): SessionEventExcerpt | null {
|
|
649
|
+
const source = typeof entry.source === "string" ? entry.source : "unknown";
|
|
650
|
+
const type = typeof entry.type === "string" ? entry.type : "unknown";
|
|
651
|
+
|
|
652
|
+
// text entries: {source, type: "text", message}
|
|
653
|
+
if (type === EVENT_TYPES.text && typeof entry.message === "string") {
|
|
654
|
+
return {
|
|
655
|
+
source,
|
|
656
|
+
type,
|
|
657
|
+
content: truncate(entry.message, MAX_EVENT_CONTENT_CHARS),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// tool_result entries: {source: "tool", type: "tool_result", result: {toolName, message, ...}}
|
|
661
|
+
// `typeof null === "object"` so we must explicitly reject null
|
|
662
|
+
// to avoid a NullPointerException-style crash when accessing
|
|
663
|
+
// r.toolName below.
|
|
664
|
+
if (type === EVENT_TYPES.toolResult && isRecord(entry.result)) {
|
|
665
|
+
const r = entry.result as Record<string, unknown>;
|
|
666
|
+
const toolName = typeof r.toolName === "string" ? r.toolName : "tool";
|
|
667
|
+
const label = (typeof r.title === "string" && r.title) || (typeof r.message === "string" && r.message) || "(no message)";
|
|
668
|
+
return {
|
|
669
|
+
source,
|
|
670
|
+
type,
|
|
671
|
+
content: `${toolName}: ${truncate(String(label), MAX_EVENT_CONTENT_CHARS - toolName.length - 2)}`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Pull workspace-relative artifact paths out of a jsonl entry. The
|
|
678
|
+
// extraction is tool-aware: different plugins stash file paths in
|
|
679
|
+
// different places inside their tool_result data. Exported for
|
|
680
|
+
// tests.
|
|
681
|
+
export function extractArtifactPaths(entry: Record<string, unknown>): string[] {
|
|
682
|
+
if (entry.type !== "tool_result") return [];
|
|
683
|
+
const result = entry.result;
|
|
684
|
+
if (!isRecord(result)) return [];
|
|
685
|
+
const r = result as Record<string, unknown>;
|
|
686
|
+
const data = r.data;
|
|
687
|
+
if (!isRecord(data)) return [];
|
|
688
|
+
const d = data as Record<string, unknown>;
|
|
689
|
+
const paths: string[] = [];
|
|
690
|
+
|
|
691
|
+
// Direct `filePath: string` — presentMulmoScript, presentHtml.
|
|
692
|
+
if (typeof d.filePath === "string" && d.filePath.length > 0) {
|
|
693
|
+
paths.push(d.filePath);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Wiki uses `pageName: string` and stores the page at
|
|
697
|
+
// `wiki/pages/<pageName>.md`. The plugin itself doesn't surface
|
|
698
|
+
// the full path in the result, so we synthesise it from the
|
|
699
|
+
// convention established in server/routes/wiki.ts.
|
|
700
|
+
if (r.toolName === "manageWiki" && typeof d.pageName === "string") {
|
|
701
|
+
paths.push(`wiki/pages/${d.pageName}.md`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Paths must be workspace-relative (not absolute, no escape).
|
|
705
|
+
// Drop anything suspicious rather than link to it.
|
|
706
|
+
return paths.filter(isSafeWorkspacePath);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Defensive: refuse absolute paths, parent-escapes, or scheme-like
|
|
710
|
+
// strings. Protects against a malformed tool result wedging a
|
|
711
|
+
// filesystem-absolute path into the archivist prompt.
|
|
712
|
+
function isSafeWorkspacePath(p: string): boolean {
|
|
713
|
+
if (!p) return false;
|
|
714
|
+
if (p.startsWith("/")) return false;
|
|
715
|
+
if (p.startsWith("..")) return false;
|
|
716
|
+
if (p.includes("://")) return false;
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function truncate(s: string, max: number): string {
|
|
721
|
+
if (max <= 0) return "";
|
|
722
|
+
if (s.length <= max) return s;
|
|
723
|
+
return `${s.slice(0, max - 1)}…`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function readAllTopics(workspaceRoot: string): Promise<ExistingTopicSnapshot[]> {
|
|
727
|
+
const topicMap = await readAllTopicFiles(workspaceRoot);
|
|
728
|
+
const out: ExistingTopicSnapshot[] = [];
|
|
729
|
+
for (const [slug, content] of topicMap) {
|
|
730
|
+
out.push({ slug, content });
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function applyTopicUpdate(workspaceRoot: string, update: TopicUpdate): Promise<"created" | "updated"> {
|
|
736
|
+
if (update.action === "create" || update.action === "append") {
|
|
737
|
+
return appendOrCreateTopic(update.slug, update.content, workspaceRoot);
|
|
738
|
+
}
|
|
739
|
+
// rewrite
|
|
740
|
+
const existed = (await readTopicFile(update.slug, workspaceRoot)) !== null;
|
|
741
|
+
await writeTopicFile(update.slug, update.content, workspaceRoot);
|
|
742
|
+
return existed ? "updated" : "created";
|
|
743
|
+
}
|