@loicngr/kobo 2.1.7 → 2.1.10
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/AGENTS.md +18 -0
- package/CHANGELOG.md +13 -0
- package/CONFIGURATION.md +44 -1
- package/DESIGN.md +11 -1
- package/README.md +4 -0
- package/dist/mcp-server/kobo-tasks-server.js +33 -1
- package/dist/mcp-server/memory-client.js +39 -0
- package/dist/mcp-server/workspace-dialogue-client.js +4 -1
- package/dist/server/db/memory-schema.js +185 -0
- package/dist/server/db/migrations.js +6 -0
- package/dist/server/db/schema.js +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/routes/mcp.js +31 -2
- package/dist/server/routes/memory.js +510 -0
- package/dist/server/routes/settings.js +3 -0
- package/dist/server/routes/workspaces.js +2 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +7 -3
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +23 -3
- package/dist/server/services/agent/engines/codex/event-mapper.js +2 -2
- package/dist/server/services/agent/orchestrator.js +110 -6
- package/dist/server/services/memory-agent-runtime.js +65 -0
- package/dist/server/services/memory-budget-service.js +205 -0
- package/dist/server/services/memory-context-service.js +306 -0
- package/dist/server/services/memory-mcp-service.js +310 -0
- package/dist/server/services/memory-service.js +891 -0
- package/dist/server/services/settings-defaults.js +2 -2
- package/dist/server/services/settings-service.js +21 -2
- package/dist/server/utils/memory-project-identity.js +17 -0
- package/dist/server/utils/memory-token-budget.js +226 -0
- package/dist/shared/codex-models.js +24 -0
- package/dist/shared/memory-prompts.js +17 -0
- package/dist/shared/memory-tools.js +215 -0
- package/dist/shared/memory.js +81 -0
- package/dist/shared/models.js +42 -0
- package/dist/shared/subagent-classification.js +5 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CkiYsNd5.js +2 -0
- package/src/client/dist/pwa/assets/{ActivityFeed-BfMUdSXQ.css → ActivityFeed-DdJvMzRI.css} +1 -1
- package/src/client/dist/pwa/assets/ChangelogPage-DkoB2IEa.js +1 -0
- package/src/client/dist/pwa/assets/ClosePopup-CRV2hxHS.js +1 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-DH9Q0XNK.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-Com5PQPV.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-DOeZUleS.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-Bke40k8T.js +8 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-CO16-Q1X.js +1 -0
- package/src/client/dist/pwa/assets/{DrawerToggleButton-CQVSfKpY.js → DrawerToggleButton-fjWJjVF_.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-BAgnXzXP.js +3 -0
- package/src/client/dist/pwa/assets/HealthPage-DR-ywiB2.js +1 -0
- package/src/client/dist/pwa/assets/MainLayout-B1suu_Nn.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-CFLswVqP.css → MainLayout-DAcSwgUw.css} +1 -1
- package/src/client/dist/pwa/assets/MemoryPanel-Ch3qeyQw.css +1 -0
- package/src/client/dist/pwa/assets/MemoryPanel-DKp9mn5X.js +1 -0
- package/src/client/dist/pwa/assets/MemoryProposalList-B3v7marz.js +1 -0
- package/src/client/dist/pwa/assets/MemoryProposalList-OmDpl3o5.css +1 -0
- package/src/client/dist/pwa/assets/QBadge-CW3Lw-kL.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-BT2ZemYu.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-a6q0PVGm.js +1 -0
- package/src/client/dist/pwa/assets/QBtnGroup-CnM4vSJA.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-DccOOtbh.js +1 -0
- package/src/client/dist/pwa/assets/QChip-Be16M4Ig.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-2fylQBjy.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-MdJ1_ZK7.js +1 -0
- package/src/client/dist/pwa/assets/QList-Cx1UfaUm.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-UhP7fVUp.js +1 -0
- package/src/client/dist/pwa/assets/QPage-CqkGHokx.js +1 -0
- package/src/client/dist/pwa/assets/QScrollArea-I9pplcfM.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-CWXNtFAO.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-xdWyaE1i.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-fzzd3anS.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-4KNBIQvY.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CGul0ui9.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-C1dcMqCO.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-_h4xQlof.js +9 -0
- package/src/client/dist/pwa/assets/SplitWorkspacePage-DxifLUAa.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-auHWDK9i.js → TerminalPanel-CRo3ETg_.js} +10 -10
- package/src/client/dist/pwa/assets/ToolCallItem-i60zEGsY.js +8 -0
- package/src/client/dist/pwa/assets/{ToolCallItem-DhjdG8vp.css → ToolCallItem-m-YnWEbv.css} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-DjFq7Kjf.js +1 -0
- package/src/client/dist/pwa/assets/{TourReplayButton-uCZpvYlj.js → TourReplayButton-DmEgJ_5R.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-C7MLWyKy.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Bw7yTZ3h.js +4 -0
- package/src/client/dist/pwa/assets/{browser-CU7taghA.js → browser-3unJ6AQX.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-CPo0G1mj.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-C5fGFFdT.js +1 -0
- package/src/client/dist/pwa/assets/de-B3oUHpbN.js +17 -0
- package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +1 -0
- package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +2 -0
- package/src/client/dist/pwa/assets/{editor.api-CBaHnF7Q.js → editor.api-CMBx0cvf.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-Bjrcpo2R.js +9 -0
- package/src/client/dist/pwa/assets/es-X1ki-Z2w.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-cR43uBa5.js +1 -0
- package/src/client/dist/pwa/assets/{format-MtzcnR3z.js → format-B1k_qdxW.js} +1 -1
- package/src/client/dist/pwa/assets/formatters-gz0Rg7Rw.js +1 -0
- package/src/client/dist/pwa/assets/fr-nkT5LuOx.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-BtVwKuc0.js → freemarker2-C8DHgbX4.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-Bhl5yJrV.js → handlebars-BmEGWkcn.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DKXLlkkM.js → html-giJc-yQc.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-PAAy4Hkn.js +1 -0
- package/src/client/dist/pwa/assets/index-BjqJ45nb.js +20 -0
- package/src/client/dist/pwa/assets/it-Bfp_J8p0.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-Cdfy_faM.js → javascript-CwQBOtjT.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-0NoEATfB.js → liquid-BPEVDB8V.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-hvP0EAwa.js → mdx-DAa4McnC.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-Be_o6s3m.js → monaco-contributions-Ct4RhitT.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-BKnd0ZwH.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-S-KP7qhA.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-I_sWGwy6.js → notify-theme-DIqeoflM.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-BDi77lhg.js → permissionModes-DLWiBfdb.js} +1 -1
- package/src/client/dist/pwa/assets/{private.use-form-vxvbh_VN.js → private.use-form-DY1BkePL.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-i2bg2TfP.js → project-color-LZ5CKuTW.js} +1 -1
- package/src/client/dist/pwa/assets/{python-Bz-bbYMw.js → python-D8xYERv8.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CyafkF-1.js → razor-BnFyr836.js} +1 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +1 -0
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-BSLweq4l.js → typescript-DW45V56c.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-v4aAbE6K.js +1 -0
- package/src/client/dist/pwa/assets/use-dark-BHObfh0O.js +1 -0
- package/src/client/dist/pwa/assets/use-id-BrdYXce0.js +1 -0
- package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +1 -0
- package/src/client/dist/pwa/assets/use-search-index-status-DzlqDFPy.js +1 -0
- package/src/client/dist/pwa/assets/{use-slide-transition-C5eDGOmu.js → use-slide-transition-B_Nu6u-t.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-CErX5nL7.js +2 -0
- package/src/client/dist/pwa/assets/use-worktree-restore-BpaEGfds.js +1 -0
- package/src/client/dist/pwa/assets/workspace-templates-CKduJ2zA.js +1 -0
- package/src/client/dist/pwa/assets/{xml-DOY7F605.js → xml-gQTv5O31.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CnWF0ZRJ.js → yaml-B4TEYUEa.js} +1 -1
- package/src/client/dist/pwa/index.html +10 -10
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +25 -0
- package/src/mcp-server/kobo-tasks-server.ts +38 -1
- package/src/mcp-server/memory-client.ts +52 -0
- package/src/mcp-server/workspace-dialogue-client.ts +4 -1
- package/src/client/dist/pwa/assets/ActivityFeed-B8tJgXwD.js +0 -2
- package/src/client/dist/pwa/assets/ChangelogPage-YjWYsB_s.js +0 -1
- package/src/client/dist/pwa/assets/ClosePopup-C1a82pL9.js +0 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-azVvpfGW.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BVNtCTdA.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DydscFLA.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-DfMqxOYw.js +0 -8
- package/src/client/dist/pwa/assets/DocumentsPanel-DCGgj9pc.js +0 -1
- package/src/client/dist/pwa/assets/GitPanel-D6ZCZPbj.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-D_A6DTiH.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-CeHg73-f.js +0 -2
- package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-CtppfNDn.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-D8I04HPE.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +0 -1
- package/src/client/dist/pwa/assets/QChip-Btg1Lp_6.js +0 -1
- package/src/client/dist/pwa/assets/QExpansionItem-DmzCAbka.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-BhGnulVQ.js +0 -1
- package/src/client/dist/pwa/assets/QList-Cw2xs69W.js +0 -1
- package/src/client/dist/pwa/assets/QMarkupTable-Ck2E2TaI.js +0 -1
- package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +0 -1
- package/src/client/dist/pwa/assets/QScrollArea-BmBuij9J.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-DUuWu79L.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-Dhy3hNTO.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BCiV6rMr.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-of0gPqV4.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BPg3-Lhb.js +0 -9
- package/src/client/dist/pwa/assets/SettingsPage-CpykRKq2.css +0 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BcdPVE9B.js +0 -1
- package/src/client/dist/pwa/assets/ToolCallItem-BQmqNcfF.js +0 -8
- package/src/client/dist/pwa/assets/TouchPan-DKpZbMAf.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-jDKgkf3G.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DQ-jvLEj.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-fw6KlTB4.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-Dfi-grUv.js +0 -1
- package/src/client/dist/pwa/assets/de-C_20Oxyv.js +0 -17
- package/src/client/dist/pwa/assets/efforts-CNJXztO0.js +0 -9
- package/src/client/dist/pwa/assets/es-B3_j4CWr.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-BtpC0GKE.js +0 -1
- package/src/client/dist/pwa/assets/formatters-BZm4POkA.js +0 -1
- package/src/client/dist/pwa/assets/fr-DbPOG-xd.js +0 -17
- package/src/client/dist/pwa/assets/i18n-B9eF3pgw.js +0 -1
- package/src/client/dist/pwa/assets/i18n-CPFleoLe.js +0 -19
- package/src/client/dist/pwa/assets/index-BVJWwOvK.js +0 -2
- package/src/client/dist/pwa/assets/it--6niM3qU.js +0 -17
- package/src/client/dist/pwa/assets/latest-request-7rgsEfKZ.js +0 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DRQLCBp8.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-DVXmkvPH.js +0 -1
- package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +0 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-tuEvQ47f.js +0 -1
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-BAt9fjK-.js +0 -1
- package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +0 -1
- package/src/client/dist/pwa/assets/use-search-index-status-BzsGSHGh.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CM4E53aB.js +0 -2
- package/src/client/dist/pwa/assets/use-worktree-restore-C__ResAR.js +0 -1
- package/src/client/dist/pwa/assets/workspace-templates-TOa0yVyr.js +0 -1
|
@@ -11,6 +11,8 @@ import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
|
11
11
|
import * as cronService from '../cron-service.js';
|
|
12
12
|
import { resolveForge } from '../forge/resolve.js';
|
|
13
13
|
import * as lifecycleHookService from '../lifecycle-hook-service.js';
|
|
14
|
+
import { advanceMemoryConversationEpoch, allocateMemoryConversationKey, bindMemoryNativeConversation, createMemoryCapability, revokeMemoryCapability, } from '../memory-agent-runtime.js';
|
|
15
|
+
import { buildMemoryContext, markMemoryContextFailed, markMemoryContextInitialized, markMemoryContextSubmitted, } from '../memory-context-service.js';
|
|
14
16
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
15
17
|
import { buildReviewReturnPrompt, getReviewReturn, restoreReviewConfiguration } from '../review-return-service.js';
|
|
16
18
|
import { activateSession, SESSION_RECENCY_ORDER } from '../session-activity-service.js';
|
|
@@ -486,7 +488,7 @@ function readEffectiveSettingsSafe(projectPath) {
|
|
|
486
488
|
};
|
|
487
489
|
}
|
|
488
490
|
}
|
|
489
|
-
function buildMcpServers(workspaceId, extraEnv) {
|
|
491
|
+
function buildMcpServers(workspaceId, extraEnv, memoryCapability) {
|
|
490
492
|
const mcpServerCompiled = getCompiledMcpServerPath();
|
|
491
493
|
const mcpServerSource = getMcpServerSourcePath();
|
|
492
494
|
return [
|
|
@@ -497,6 +499,7 @@ function buildMcpServers(workspaceId, extraEnv) {
|
|
|
497
499
|
env: {
|
|
498
500
|
KOBO_WORKSPACE_ID: workspaceId,
|
|
499
501
|
...extraEnv,
|
|
502
|
+
...(memoryCapability ? { KOBO_MEMORY_SESSION_TOKEN: memoryCapability } : {}),
|
|
500
503
|
KOBO_DB_PATH: getDbPath(),
|
|
501
504
|
KOBO_SETTINGS_PATH: getSettingsPath(),
|
|
502
505
|
KOBO_BACKEND_URL: `http://127.0.0.1:${backendPort}`,
|
|
@@ -842,6 +845,13 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
842
845
|
// stop — see `sourceControllerIsStopping` further down.
|
|
843
846
|
if (sourceNoLongerOwnsWorkspace && ev.kind !== 'session:ended')
|
|
844
847
|
return;
|
|
848
|
+
if (sourceController && ev.kind === 'session:started') {
|
|
849
|
+
const conversationKey = controllerMemoryConversationKeys.get(sourceController);
|
|
850
|
+
if (conversationKey)
|
|
851
|
+
bindMemoryNativeConversation(conversationKey, ev.engineSessionId);
|
|
852
|
+
}
|
|
853
|
+
if (sourceController && ev.kind === 'session:ended' && terminalAfterClose)
|
|
854
|
+
revokeControllerMemoryCapability(sourceController);
|
|
845
855
|
// A controller already told to stop (D1) keeps emitting real events until it
|
|
846
856
|
// actually dies — they are persisted and broadcast like any other (see
|
|
847
857
|
// `routeEvent` below, unconditional). But a handful of side effects here
|
|
@@ -861,6 +871,34 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
861
871
|
if (launch?.handoffGeneration &&
|
|
862
872
|
!['session:started', 'session:ended', 'session:user-input-requested'].includes(ev.kind))
|
|
863
873
|
return;
|
|
874
|
+
if (sourceController &&
|
|
875
|
+
!sourceControllerIsStopping &&
|
|
876
|
+
controllers.get(workspaceId) === sourceController &&
|
|
877
|
+
pendingStarts.get(workspaceId) === undefined) {
|
|
878
|
+
const compaction = memoryCompactionStates.get(sourceController) ?? { active: false, compactedWithoutId: false };
|
|
879
|
+
if (ev.kind === 'session:compacting' && ev.active) {
|
|
880
|
+
compaction.active = true;
|
|
881
|
+
compaction.compactedWithoutId = false;
|
|
882
|
+
if (ev.compactionId)
|
|
883
|
+
compaction.activeId = ev.compactionId;
|
|
884
|
+
memoryCompactionStates.set(sourceController, compaction);
|
|
885
|
+
}
|
|
886
|
+
else if (ev.kind === 'session:compacted') {
|
|
887
|
+
const duplicateId = ev.compactionId !== undefined && ev.compactionId === compaction.lastId;
|
|
888
|
+
const hasDistinctSignal = ev.compactionId !== undefined ? !duplicateId : compaction.active || !compaction.compactedWithoutId;
|
|
889
|
+
if (hasDistinctSignal) {
|
|
890
|
+
const conversationKey = controllerMemoryConversationKeys.get(sourceController);
|
|
891
|
+
if (conversationKey)
|
|
892
|
+
advanceMemoryConversationEpoch(conversationKey);
|
|
893
|
+
if (ev.compactionId)
|
|
894
|
+
compaction.lastId = ev.compactionId;
|
|
895
|
+
compaction.compactedWithoutId = true;
|
|
896
|
+
}
|
|
897
|
+
compaction.active = false;
|
|
898
|
+
compaction.activeId = undefined;
|
|
899
|
+
memoryCompactionStates.set(sourceController, compaction);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
864
902
|
if (sourceController && !sourceControllerIsStopping) {
|
|
865
903
|
if (ev.kind === 'session:compacting' && ev.active) {
|
|
866
904
|
const status = getWs(workspaceId)?.status;
|
|
@@ -1298,6 +1336,17 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1298
1336
|
return true;
|
|
1299
1337
|
}
|
|
1300
1338
|
const launchOptions = new WeakMap();
|
|
1339
|
+
const controllerMemoryCapabilities = new WeakMap();
|
|
1340
|
+
const controllerMemoryConversationKeys = new WeakMap();
|
|
1341
|
+
const controllerMemoryContextIds = new WeakMap();
|
|
1342
|
+
const memoryCompactionStates = new WeakMap();
|
|
1343
|
+
function revokeControllerMemoryCapability(controller) {
|
|
1344
|
+
const token = controllerMemoryCapabilities.get(controller);
|
|
1345
|
+
if (!token)
|
|
1346
|
+
return;
|
|
1347
|
+
revokeMemoryCapability(token);
|
|
1348
|
+
controllerMemoryCapabilities.delete(controller);
|
|
1349
|
+
}
|
|
1301
1350
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
1302
1351
|
/**
|
|
1303
1352
|
* Spawn an agent (via the resolved engine) for a workspace. Returns
|
|
@@ -1384,6 +1433,22 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1384
1433
|
// A review whose report returns to the original session must end by itself:
|
|
1385
1434
|
// enforce read-only plan mode here, not only through its prompt.
|
|
1386
1435
|
const readOnly = getReviewReturn(workspaceId, agentSessionId) !== null;
|
|
1436
|
+
const effectivePermissionMode = readOnly ? 'plan' : (agentPermissionMode ?? ws?.agentPermissionMode ?? 'bypass');
|
|
1437
|
+
const memoryDispatchId = nanoid();
|
|
1438
|
+
const memoryCapability = createMemoryCapability({
|
|
1439
|
+
dispatchId: memoryDispatchId,
|
|
1440
|
+
workspaceId,
|
|
1441
|
+
sessionId: agentSessionId,
|
|
1442
|
+
engine: engine.id,
|
|
1443
|
+
// A resume continues its prior native conversation; a fresh dispatch starts
|
|
1444
|
+
// a separate context even if application session selection is reused.
|
|
1445
|
+
conversationKey: allocateMemoryConversationKey({
|
|
1446
|
+
sessionId: agentSessionId,
|
|
1447
|
+
engine: engine.id,
|
|
1448
|
+
...(resumeFromEngineSessionId ? { nativeConversationId: resumeFromEngineSessionId } : {}),
|
|
1449
|
+
}),
|
|
1450
|
+
readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration,
|
|
1451
|
+
});
|
|
1387
1452
|
const unavailableIntegrations = [];
|
|
1388
1453
|
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
|
|
1389
1454
|
const options = {
|
|
@@ -1393,7 +1458,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1393
1458
|
model,
|
|
1394
1459
|
effort: reasoningEffort,
|
|
1395
1460
|
// Cascade: explicit caller override → workspace setting → 'bypass'.
|
|
1396
|
-
agentPermissionMode:
|
|
1461
|
+
agentPermissionMode: effectivePermissionMode,
|
|
1397
1462
|
readOnly,
|
|
1398
1463
|
resumeFromEngineSessionId,
|
|
1399
1464
|
backendUrl: `http://127.0.0.1:${backendPort}`,
|
|
@@ -1406,12 +1471,23 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1406
1471
|
}
|
|
1407
1472
|
})(),
|
|
1408
1473
|
settings,
|
|
1409
|
-
mcpServers: [...buildMcpServers(workspaceId, launch?.mcpEnv), ...integrationServers],
|
|
1474
|
+
mcpServers: [...buildMcpServers(workspaceId, launch?.mcpEnv, memoryCapability.token), ...integrationServers],
|
|
1410
1475
|
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
1411
1476
|
};
|
|
1412
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
beforeDispatch?.();
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
revokeMemoryCapability(memoryCapability.token);
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1413
1484
|
let controller;
|
|
1414
1485
|
controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, controller, ev));
|
|
1486
|
+
controllerMemoryCapabilities.set(controller, memoryCapability.token);
|
|
1487
|
+
controllerMemoryConversationKeys.set(controller, memoryCapability.conversationKey);
|
|
1488
|
+
// This promise settles only after the controller confirms actual process
|
|
1489
|
+
// closure, covering direct stop() paths that do not emit session:ended.
|
|
1490
|
+
void controller.closed.then(() => revokeControllerMemoryCapability(controller));
|
|
1415
1491
|
if (launch)
|
|
1416
1492
|
launchOptions.set(controller, launch);
|
|
1417
1493
|
registerSessionLifecycleOwner(workspaceId, agentSessionId, controller);
|
|
@@ -1438,11 +1514,33 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1438
1514
|
if (registered && registered !== controller)
|
|
1439
1515
|
throw new Error('Agent is still stopping');
|
|
1440
1516
|
controllers.set(workspaceId, controller);
|
|
1517
|
+
const memoryContext = launch?.handoffGeneration
|
|
1518
|
+
? undefined
|
|
1519
|
+
: buildMemoryContext({
|
|
1520
|
+
workspaceId,
|
|
1521
|
+
sessionId: agentSessionId,
|
|
1522
|
+
engine: engine.id,
|
|
1523
|
+
dispatchId: memoryDispatchId,
|
|
1524
|
+
conversationKey: memoryCapability.conversationKey,
|
|
1525
|
+
resume: Boolean(resumeFromEngineSessionId),
|
|
1526
|
+
readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration,
|
|
1527
|
+
mode: getGlobalSettings().memoryMode,
|
|
1528
|
+
});
|
|
1529
|
+
if (memoryContext) {
|
|
1530
|
+
controllerMemoryContextIds.set(controller, memoryContext.recordId);
|
|
1531
|
+
options.prompt = `${options.prompt}\n\n${memoryContext.prompt}`;
|
|
1532
|
+
markMemoryContextSubmitted(memoryContext.recordId);
|
|
1533
|
+
}
|
|
1441
1534
|
return controller.start(options);
|
|
1442
1535
|
})
|
|
1443
1536
|
.then(async () => {
|
|
1444
|
-
|
|
1445
|
-
|
|
1537
|
+
const ready = controller.engineProcess?.ready;
|
|
1538
|
+
const contextId = controllerMemoryContextIds.get(controller);
|
|
1539
|
+
if (ready && contextId) {
|
|
1540
|
+
void ready.then(() => markMemoryContextInitialized(contextId), () => markMemoryContextFailed(contextId));
|
|
1541
|
+
}
|
|
1542
|
+
if (launch?.onStarted && ready)
|
|
1543
|
+
await ready;
|
|
1446
1544
|
if (controller.status !== 'stopping')
|
|
1447
1545
|
launch?.onStarted?.();
|
|
1448
1546
|
const pid = controller.pid;
|
|
@@ -1457,6 +1555,10 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1457
1555
|
}
|
|
1458
1556
|
})
|
|
1459
1557
|
.catch((err) => {
|
|
1558
|
+
revokeControllerMemoryCapability(controller);
|
|
1559
|
+
const contextId = controllerMemoryContextIds.get(controller);
|
|
1560
|
+
if (contextId)
|
|
1561
|
+
markMemoryContextFailed(contextId);
|
|
1460
1562
|
if (existingCtrl && controller.status === 'stopping')
|
|
1461
1563
|
return;
|
|
1462
1564
|
console.error('[orchestrator] engine.start failed:', err);
|
|
@@ -1532,6 +1634,7 @@ export function stopSubagents(workspaceId, id) {
|
|
|
1532
1634
|
/** Bound the caller's wait, retaining ownership until actual engine shutdown. */
|
|
1533
1635
|
export const STOP_AGENT_TIMEOUT_MS = 15_000;
|
|
1534
1636
|
async function stopController(workspaceId, ctrl, cause = 'user') {
|
|
1637
|
+
revokeControllerMemoryCapability(ctrl);
|
|
1535
1638
|
ctrl.stopCause = cause;
|
|
1536
1639
|
restoreReviewConfiguration(workspaceId, ctrl.agentSessionId);
|
|
1537
1640
|
preCompactionStatus.delete(ctrl);
|
|
@@ -1615,6 +1718,7 @@ cause = 'user') {
|
|
|
1615
1718
|
let pendingStop = Promise.resolve();
|
|
1616
1719
|
if (pending) {
|
|
1617
1720
|
pendingStarts.delete(workspaceId);
|
|
1721
|
+
revokeControllerMemoryCapability(pending);
|
|
1618
1722
|
pending.stopCause = cause;
|
|
1619
1723
|
pendingStop = pending.stop().then(() => {
|
|
1620
1724
|
handleEvent(workspaceId, pending.agentSessionId, pending, {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { getDb } from '../db/index.js';
|
|
4
|
+
const activeCapabilities = new Map();
|
|
5
|
+
/** Persist an opaque conversation key independently from a controller/token generation. */
|
|
6
|
+
export function allocateMemoryConversationKey(input) {
|
|
7
|
+
const db = getDb();
|
|
8
|
+
if (input.nativeConversationId) {
|
|
9
|
+
const existing = db
|
|
10
|
+
.prepare(`SELECT conversation_key FROM memory_budget_contexts
|
|
11
|
+
WHERE kind = 'internal' AND session_id = ? AND engine = ? AND native_conversation_id = ?
|
|
12
|
+
ORDER BY updated_at DESC LIMIT 1`)
|
|
13
|
+
.get(input.sessionId, input.engine, input.nativeConversationId);
|
|
14
|
+
if (existing)
|
|
15
|
+
return existing.conversation_key;
|
|
16
|
+
}
|
|
17
|
+
const key = nanoid();
|
|
18
|
+
const timestamp = new Date().toISOString();
|
|
19
|
+
db.prepare(`INSERT INTO memory_budget_contexts
|
|
20
|
+
(id, kind, session_id, engine, conversation_key, native_conversation_id, epoch,
|
|
21
|
+
cumulative_estimated_tokens, delivered_json, created_at, updated_at)
|
|
22
|
+
VALUES (?, 'internal', ?, ?, ?, ?, 0, 0, '[]', ?, ?)`).run(nanoid(), input.sessionId, input.engine, key, input.nativeConversationId ?? null, timestamp, timestamp);
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
/** Bind a fresh conversation's native provider id once the engine confirms it. */
|
|
26
|
+
export function bindMemoryNativeConversation(key, nativeConversationId) {
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
getDb()
|
|
29
|
+
.prepare(`UPDATE memory_budget_contexts SET native_conversation_id = ?, updated_at = ?
|
|
30
|
+
WHERE kind = 'internal' AND conversation_key = ? AND epoch = 0 AND native_conversation_id IS NULL`)
|
|
31
|
+
.run(nativeConversationId, timestamp, key);
|
|
32
|
+
}
|
|
33
|
+
/** Start a new metadata-only ledger epoch after the owning controller confirms compaction. */
|
|
34
|
+
export function advanceMemoryConversationEpoch(key) {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const current = db
|
|
37
|
+
.prepare(`SELECT id, session_id, engine, native_conversation_id, epoch FROM memory_budget_contexts
|
|
38
|
+
WHERE kind = 'internal' AND conversation_key = ? ORDER BY epoch DESC LIMIT 1`)
|
|
39
|
+
.get(key);
|
|
40
|
+
if (!current)
|
|
41
|
+
return undefined;
|
|
42
|
+
const id = nanoid();
|
|
43
|
+
const epoch = current.epoch + 1;
|
|
44
|
+
const timestamp = new Date().toISOString();
|
|
45
|
+
db.prepare(`INSERT INTO memory_budget_contexts
|
|
46
|
+
(id, kind, session_id, engine, conversation_key, native_conversation_id, epoch,
|
|
47
|
+
cumulative_estimated_tokens, delivered_json, created_at, updated_at)
|
|
48
|
+
VALUES (?, 'internal', ?, ?, ?, ?, ?, 0, '[]', ?, ?)`).run(id, current.session_id, current.engine, key, current.native_conversation_id, epoch, timestamp, timestamp);
|
|
49
|
+
return { id, epoch };
|
|
50
|
+
}
|
|
51
|
+
/** Issue an unguessable, launch-specific capability. The raw value is process env only. */
|
|
52
|
+
export function createMemoryCapability(descriptor) {
|
|
53
|
+
const token = randomBytes(32).toString('hex');
|
|
54
|
+
activeCapabilities.set(token, Object.freeze({ ...descriptor }));
|
|
55
|
+
return { token, conversationKey: descriptor.conversationKey };
|
|
56
|
+
}
|
|
57
|
+
export function getMemoryCapability(token) {
|
|
58
|
+
if (!token || !/^[a-f0-9]{64}$/.test(token))
|
|
59
|
+
return undefined;
|
|
60
|
+
return activeCapabilities.get(token);
|
|
61
|
+
}
|
|
62
|
+
/** Revokes only this launch secret, never another generation sharing its session id. */
|
|
63
|
+
export function revokeMemoryCapability(token) {
|
|
64
|
+
activeCapabilities.delete(token);
|
|
65
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { MEMORY_CONVERSATION_BUDGET_TOKENS } from '../../shared/memory.js';
|
|
3
|
+
import { getDb } from '../db/index.js';
|
|
4
|
+
/** Metadata only: fingerprints never retain note bodies or prompt text. */
|
|
5
|
+
export function getMemoryBootstrapFingerprint(budgetContextId) {
|
|
6
|
+
const row = getDb().prepare('SELECT delivered_json FROM memory_budget_contexts WHERE id = ?').get(budgetContextId);
|
|
7
|
+
if (!row)
|
|
8
|
+
return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const records = JSON.parse(row.delivered_json);
|
|
11
|
+
if (!Array.isArray(records))
|
|
12
|
+
return undefined;
|
|
13
|
+
const record = records.reverse().find((item) => item?.kind === 'bootstrap');
|
|
14
|
+
if (typeof record?.guidanceHash === 'string' && typeof record?.snapshotHash === 'string')
|
|
15
|
+
return { guidanceHash: record.guidanceHash, snapshotHash: record.snapshotHash };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* Legacy or malformed metadata cannot prove prior delivery. */
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function getInternalMemoryBudgetContext(conversationKey) {
|
|
23
|
+
const row = getDb()
|
|
24
|
+
.prepare(`SELECT id, epoch, cumulative_estimated_tokens AS cumulativeEstimatedTokens
|
|
25
|
+
FROM memory_budget_contexts WHERE kind = 'internal' AND conversation_key = ?
|
|
26
|
+
ORDER BY epoch DESC LIMIT 1`)
|
|
27
|
+
.get(conversationKey);
|
|
28
|
+
if (!row)
|
|
29
|
+
throw new TypeError('Internal memory budget ledger not found');
|
|
30
|
+
return row;
|
|
31
|
+
}
|
|
32
|
+
const MEMORY_DENIAL_RESERVE_TOKENS = 500;
|
|
33
|
+
const MEMORY_REGULAR_OUTPUT_LIMIT = MEMORY_CONVERSATION_BUDGET_TOKENS - MEMORY_DENIAL_RESERVE_TOKENS;
|
|
34
|
+
const BUDGET_DENIAL_MARKER = 'budget-denial';
|
|
35
|
+
function now() {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
/** Create a new external handle or resolve one previously returned to the same client/transport. */
|
|
39
|
+
export function createExternalMemoryBudgetContext(requestedId, clientName, transport) {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
if (requestedId !== undefined) {
|
|
42
|
+
if (requestedId.length < 12 || requestedId.length > 128)
|
|
43
|
+
throw new TypeError('Invalid memory_context_id');
|
|
44
|
+
const row = db
|
|
45
|
+
.prepare(`SELECT id, kind, cumulative_estimated_tokens, client_name, transport
|
|
46
|
+
FROM memory_budget_contexts WHERE external_context_id = ? OR id = ?`)
|
|
47
|
+
.get(requestedId, requestedId);
|
|
48
|
+
if (row?.kind !== 'external' || row.id !== requestedId) {
|
|
49
|
+
throw new TypeError('Unknown external memory_context_id');
|
|
50
|
+
}
|
|
51
|
+
if (row.client_name !== clientName || row.transport !== transport) {
|
|
52
|
+
throw new TypeError('memory_context_id belongs to another external client context');
|
|
53
|
+
}
|
|
54
|
+
return { id: row.id, cumulativeEstimatedTokens: row.cumulative_estimated_tokens };
|
|
55
|
+
}
|
|
56
|
+
const id = nanoid(32);
|
|
57
|
+
const timestamp = now();
|
|
58
|
+
db.prepare(`INSERT INTO memory_budget_contexts
|
|
59
|
+
(id, kind, external_context_id, epoch, cumulative_estimated_tokens, delivered_json, client_name, transport, created_at, updated_at)
|
|
60
|
+
VALUES (?, 'external', ?, 0, 0, '[]', ?, ?, ?, ?)`).run(id, id, clientName.slice(0, 120), transport, timestamp, timestamp);
|
|
61
|
+
return { id, cumulativeEstimatedTokens: 0 };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Charges a native-conversation bootstrap or an MCP response using an immediate
|
|
65
|
+
* SQLite transaction, so concurrent sub-agents cannot spend the same allowance.
|
|
66
|
+
* Unknown/failed delivery is intentionally still charged.
|
|
67
|
+
*/
|
|
68
|
+
export function reserveMemoryBudget(input) {
|
|
69
|
+
if (!Number.isSafeInteger(input.estimatedTokens) || input.estimatedTokens < 1)
|
|
70
|
+
throw new TypeError('Memory budget charge must be a positive integer');
|
|
71
|
+
const responseLimitTokens = input.responseLimitTokens;
|
|
72
|
+
if (responseLimitTokens !== undefined && input.estimatedTokens > responseLimitTokens) {
|
|
73
|
+
return {
|
|
74
|
+
accepted: false,
|
|
75
|
+
budgetContextId: input.budgetContextId,
|
|
76
|
+
estimatedTokens: input.estimatedTokens,
|
|
77
|
+
remainingTokens: 0,
|
|
78
|
+
limitTokens: responseLimitTokens,
|
|
79
|
+
reason: 'response-limit',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const db = getDb();
|
|
83
|
+
return db
|
|
84
|
+
.transaction(() => {
|
|
85
|
+
const row = db
|
|
86
|
+
.prepare(`SELECT id, kind, cumulative_estimated_tokens, delivered_json
|
|
87
|
+
FROM memory_budget_contexts WHERE id = ?`)
|
|
88
|
+
.get(input.budgetContextId);
|
|
89
|
+
if (!row)
|
|
90
|
+
throw new TypeError('Memory budget ledger not found');
|
|
91
|
+
let delivered;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(row.delivered_json);
|
|
94
|
+
delivered = Array.isArray(parsed) ? parsed : [];
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
delivered = [];
|
|
98
|
+
}
|
|
99
|
+
const denialAlreadyUsed = delivered.some((item) => item && typeof item === 'object' && item.kind === BUDGET_DENIAL_MARKER);
|
|
100
|
+
const remaining = denialAlreadyUsed
|
|
101
|
+
? 0
|
|
102
|
+
: Math.max(0, MEMORY_REGULAR_OUTPUT_LIMIT - row.cumulative_estimated_tokens);
|
|
103
|
+
if (input.estimatedTokens > remaining) {
|
|
104
|
+
return {
|
|
105
|
+
accepted: false,
|
|
106
|
+
budgetContextId: row.id,
|
|
107
|
+
estimatedTokens: input.estimatedTokens,
|
|
108
|
+
remainingTokens: remaining,
|
|
109
|
+
limitTokens: MEMORY_REGULAR_OUTPUT_LIMIT,
|
|
110
|
+
reason: 'conversation-limit',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const newDeliveries = [...(input.deliveries ?? []), ...(input.delivery ? [input.delivery] : [])];
|
|
114
|
+
if (input.bootstrap) {
|
|
115
|
+
delivered = delivered.filter((item) => !item || typeof item !== 'object' || item.kind !== 'bootstrap');
|
|
116
|
+
delivered.push({ kind: 'bootstrap', ...input.bootstrap });
|
|
117
|
+
}
|
|
118
|
+
if (newDeliveries.length > 0) {
|
|
119
|
+
delivered.push(...newDeliveries);
|
|
120
|
+
if (delivered.length > 2_000)
|
|
121
|
+
delivered = delivered.slice(-2_000);
|
|
122
|
+
}
|
|
123
|
+
const cumulative = row.cumulative_estimated_tokens + input.estimatedTokens;
|
|
124
|
+
db.prepare(`UPDATE memory_budget_contexts SET cumulative_estimated_tokens = ?, delivered_json = ?, updated_at = ?
|
|
125
|
+
WHERE id = ?`).run(cumulative, JSON.stringify(delivered), now(), row.id);
|
|
126
|
+
return {
|
|
127
|
+
accepted: true,
|
|
128
|
+
budgetContextId: row.id,
|
|
129
|
+
estimatedTokens: input.estimatedTokens,
|
|
130
|
+
remainingTokens: MEMORY_REGULAR_OUTPUT_LIMIT - cumulative,
|
|
131
|
+
limitTokens: MEMORY_REGULAR_OUTPUT_LIMIT,
|
|
132
|
+
};
|
|
133
|
+
})
|
|
134
|
+
.immediate();
|
|
135
|
+
}
|
|
136
|
+
/** Spend one reserved terminal receipt; later exhausted calls are suppressed without output. */
|
|
137
|
+
export function reserveMemoryBudgetDenial(budgetContextId, estimatedTokens) {
|
|
138
|
+
if (!Number.isSafeInteger(estimatedTokens) || estimatedTokens < 1 || estimatedTokens > MEMORY_DENIAL_RESERVE_TOKENS)
|
|
139
|
+
throw new TypeError('Memory budget denial receipt exceeds its reserved allowance');
|
|
140
|
+
const db = getDb();
|
|
141
|
+
return db
|
|
142
|
+
.transaction(() => {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare(`SELECT id, cumulative_estimated_tokens, delivered_json
|
|
145
|
+
FROM memory_budget_contexts WHERE id = ?`)
|
|
146
|
+
.get(budgetContextId);
|
|
147
|
+
if (!row)
|
|
148
|
+
throw new TypeError('Memory budget ledger not found');
|
|
149
|
+
let delivered;
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(row.delivered_json);
|
|
152
|
+
delivered = Array.isArray(parsed) ? parsed : [];
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
delivered = [];
|
|
156
|
+
}
|
|
157
|
+
const alreadyUsed = delivered.some((item) => item && typeof item === 'object' && item.kind === BUDGET_DENIAL_MARKER);
|
|
158
|
+
if (alreadyUsed || row.cumulative_estimated_tokens + estimatedTokens > MEMORY_CONVERSATION_BUDGET_TOKENS) {
|
|
159
|
+
return {
|
|
160
|
+
responseAllowed: false,
|
|
161
|
+
budgetContextId: row.id,
|
|
162
|
+
estimatedTokens: 0,
|
|
163
|
+
remainingTokens: Math.max(0, MEMORY_CONVERSATION_BUDGET_TOKENS - row.cumulative_estimated_tokens),
|
|
164
|
+
limitTokens: MEMORY_CONVERSATION_BUDGET_TOKENS,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
delivered.push({ kind: BUDGET_DENIAL_MARKER, estimatedTokens });
|
|
168
|
+
const cumulative = row.cumulative_estimated_tokens + estimatedTokens;
|
|
169
|
+
db.prepare(`UPDATE memory_budget_contexts
|
|
170
|
+
SET cumulative_estimated_tokens = ?, delivered_json = ?, updated_at = ? WHERE id = ?`).run(cumulative, JSON.stringify(delivered), now(), row.id);
|
|
171
|
+
return {
|
|
172
|
+
responseAllowed: true,
|
|
173
|
+
budgetContextId: row.id,
|
|
174
|
+
estimatedTokens,
|
|
175
|
+
remainingTokens: MEMORY_CONVERSATION_BUDGET_TOKENS - cumulative,
|
|
176
|
+
limitTokens: MEMORY_CONVERSATION_BUDGET_TOKENS,
|
|
177
|
+
};
|
|
178
|
+
})
|
|
179
|
+
.immediate();
|
|
180
|
+
}
|
|
181
|
+
/** Returns delivery metadata only; persisted note bodies are never stored in this ledger. */
|
|
182
|
+
export function wasMemoryFragmentDelivered(budgetContextId, delivery) {
|
|
183
|
+
const row = getDb()
|
|
184
|
+
.prepare('SELECT kind, delivered_json FROM memory_budget_contexts WHERE id = ?')
|
|
185
|
+
.get(budgetContextId);
|
|
186
|
+
if (!row)
|
|
187
|
+
return false;
|
|
188
|
+
try {
|
|
189
|
+
const items = JSON.parse(row.delivered_json);
|
|
190
|
+
return (Array.isArray(items) &&
|
|
191
|
+
items.some((item) => {
|
|
192
|
+
if (!item || typeof item !== 'object')
|
|
193
|
+
return false;
|
|
194
|
+
const candidate = item;
|
|
195
|
+
return (candidate.entryId === delivery.entryId &&
|
|
196
|
+
candidate.revision === delivery.revision &&
|
|
197
|
+
candidate.kind === delivery.kind &&
|
|
198
|
+
candidate.start === delivery.start &&
|
|
199
|
+
candidate.end === delivery.end);
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|