@gmickel/gno 2.5.1 → 2.7.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 +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +80 -4
- package/assets/skill/cli-reference.md +132 -2
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +54 -1
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +449 -35
- package/spec/mcp.md +234 -4
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +4 -1
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
- package/spec/output-schemas/memory-remember.schema.json +10 -4
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +441 -2
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +12 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +234 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/embed/backlog.ts +124 -18
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +15 -10
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +30 -2
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +17 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +87 -85
- package/src/mcp/tools/index.ts +77 -4
- package/src/mcp/tools/memory-remember.ts +8 -1
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/sdk/client.ts +180 -84
- package/src/sdk/index.ts +6 -0
- package/src/sdk/types.ts +54 -2
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/app.tsx +4 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +26 -3
- package/src/serve/public/lib/request-intent.ts +77 -0
- package/src/serve/public/lib/snippet.tsx +52 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +22 -9
- package/src/serve/public/pages/DocView.tsx +25 -6
- package/src/serve/public/pages/DocumentEditor.tsx +224 -104
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +69 -4
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +215 -10
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/status-model.ts +16 -0
- package/src/serve/status.ts +2 -0
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +76 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
package/src/serve/security.ts
CHANGED
|
@@ -69,6 +69,15 @@ export function isRequestAllowed(req: Request, port: number): boolean {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Unsafe methods - require valid Origin or token
|
|
72
|
+
return isOriginOrTokenAllowed(req, port);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Origin/token check without the safe-method exemption. Owner-only reads
|
|
77
|
+
* that return host paths use it so a cross-origin page is refused on GET
|
|
78
|
+
* the same way as on POST.
|
|
79
|
+
*/
|
|
80
|
+
export function isOriginOrTokenAllowed(req: Request, port: number): boolean {
|
|
72
81
|
return validateToken(req) || validateOrigin(req, port);
|
|
73
82
|
}
|
|
74
83
|
|
package/src/serve/server.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { HttpGatewayOverrides } from "../mcp/http-security";
|
|
2
|
-
/**
|
|
3
|
-
* Bun.serve() web server for GNO web UI.
|
|
4
|
-
* Uses Bun's fullstack dev server with HTML imports.
|
|
5
|
-
* Opens DB once at startup, closes on shutdown.
|
|
6
|
-
*
|
|
7
|
-
* @module src/serve/server
|
|
8
|
-
*/
|
|
9
2
|
import type { RequestPeerServer } from "./request-locality";
|
|
10
3
|
import type { ResidentRuntime } from "./resident-runtime";
|
|
11
4
|
import type { ContextHolder } from "./routes/api";
|
|
@@ -23,6 +16,14 @@ import {
|
|
|
23
16
|
handlePdfjsVendorRequest,
|
|
24
17
|
isPdfjsVendorPath,
|
|
25
18
|
} from "./fn112-routes";
|
|
19
|
+
/**
|
|
20
|
+
* Bun.serve() web server for GNO web UI.
|
|
21
|
+
* Uses Bun's fullstack dev server with HTML imports.
|
|
22
|
+
* Opens DB once at startup, closes on shutdown.
|
|
23
|
+
*
|
|
24
|
+
* @module src/serve/server
|
|
25
|
+
*/
|
|
26
|
+
import { withRemoteHostPathRedaction } from "./host-path-redaction";
|
|
26
27
|
import { PDFJS_ASSET_CACHE_CONTROL } from "./pdfjs-assets";
|
|
27
28
|
// HTML import - Bun handles bundling TSX/CSS automatically via routes
|
|
28
29
|
import homepage from "./public/index.html";
|
|
@@ -82,6 +83,7 @@ import {
|
|
|
82
83
|
handleTrashDoc,
|
|
83
84
|
handleUpdateCollection,
|
|
84
85
|
handleUpdateCollectionEgressPolicy,
|
|
86
|
+
handleRequestStatus,
|
|
85
87
|
handleUpdateDoc,
|
|
86
88
|
handleVerifyConnector,
|
|
87
89
|
} from "./routes/api";
|
|
@@ -101,6 +103,24 @@ import {
|
|
|
101
103
|
handleCreateSectionTarget,
|
|
102
104
|
handleResolveSectionTarget,
|
|
103
105
|
} from "./routes/section-targets";
|
|
106
|
+
import {
|
|
107
|
+
handleSessionsAddSource,
|
|
108
|
+
handleSessionsAutomationDisable,
|
|
109
|
+
handleSessionsAutomationEnable,
|
|
110
|
+
handleSessionsAutomationPreview,
|
|
111
|
+
handleSessionsAutomationRemove,
|
|
112
|
+
handleSessionsAutomationRun,
|
|
113
|
+
handleSessionsAutomationSet,
|
|
114
|
+
handleSessionsDiscover,
|
|
115
|
+
handleSessionsImport,
|
|
116
|
+
handleSessionsInit,
|
|
117
|
+
handleSessionsRemoveSource,
|
|
118
|
+
handleSessionsStatus,
|
|
119
|
+
} from "./routes/sessions";
|
|
120
|
+
|
|
121
|
+
/** `/api/sessions/automation/:id[/...]` path parameter. */
|
|
122
|
+
const automationProfileId = (req: Request): string =>
|
|
123
|
+
decodeURIComponent(new URL(req.url).pathname.split("/")[4] ?? "");
|
|
104
124
|
import {
|
|
105
125
|
handleTraceDelete,
|
|
106
126
|
handleTraceExport,
|
|
@@ -109,7 +129,11 @@ import {
|
|
|
109
129
|
handleTracePurge,
|
|
110
130
|
handleTraceShow,
|
|
111
131
|
} from "./routes/traces";
|
|
112
|
-
import {
|
|
132
|
+
import {
|
|
133
|
+
forbiddenResponse,
|
|
134
|
+
isOriginOrTokenAllowed,
|
|
135
|
+
isRequestAllowed,
|
|
136
|
+
} from "./security";
|
|
113
137
|
import {
|
|
114
138
|
createSpaBundleSource,
|
|
115
139
|
type SpaBundleSource,
|
|
@@ -548,7 +572,7 @@ export async function startServer(
|
|
|
548
572
|
development: isDev,
|
|
549
573
|
|
|
550
574
|
// Static routes - Bun handles HTML bundling and /_bun/* assets automatically
|
|
551
|
-
routes: {
|
|
575
|
+
routes: withRemoteHostPathRedaction({
|
|
552
576
|
"/mcp": gateway.route,
|
|
553
577
|
...clipperRoutesForBind(
|
|
554
578
|
isHttpGatewayLoopbackBind(gatewayConfig.host),
|
|
@@ -566,6 +590,7 @@ export async function startServer(
|
|
|
566
590
|
"/collections": spaPageRoute,
|
|
567
591
|
"/connectors": spaPageRoute,
|
|
568
592
|
"/traces": spaPageRoute,
|
|
593
|
+
"/sessions": spaPageRoute,
|
|
569
594
|
"/context/compiled": spaPageRoute,
|
|
570
595
|
"/ask": spaPageRoute,
|
|
571
596
|
"/graph": spaPageRoute,
|
|
@@ -833,6 +858,174 @@ export async function startServer(
|
|
|
833
858
|
);
|
|
834
859
|
},
|
|
835
860
|
},
|
|
861
|
+
"/api/sessions/status": {
|
|
862
|
+
GET: async (req: Request) =>
|
|
863
|
+
withSecurityHeaders(
|
|
864
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
865
|
+
handleSessionsStatus(ctxHolder)
|
|
866
|
+
),
|
|
867
|
+
isDev
|
|
868
|
+
),
|
|
869
|
+
},
|
|
870
|
+
"/api/sessions/discover": {
|
|
871
|
+
GET: async (req: Request, server: RequestPeerServer) => {
|
|
872
|
+
// Returns host paths: a cross-origin page is refused like a write.
|
|
873
|
+
if (!isOriginOrTokenAllowed(req, port)) {
|
|
874
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
875
|
+
}
|
|
876
|
+
return withSecurityHeaders(
|
|
877
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
878
|
+
handleSessionsDiscover(ctxHolder, req, { server })
|
|
879
|
+
),
|
|
880
|
+
isDev
|
|
881
|
+
);
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
"/api/sessions/import": {
|
|
885
|
+
POST: async (req: Request) => {
|
|
886
|
+
if (!isRequestAllowed(req, port)) {
|
|
887
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
888
|
+
}
|
|
889
|
+
return withSecurityHeaders(
|
|
890
|
+
await handleSessionsImport(ctxHolder, req),
|
|
891
|
+
isDev
|
|
892
|
+
);
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
"/api/sessions/sources": {
|
|
896
|
+
POST: async (req: Request, server: RequestPeerServer) => {
|
|
897
|
+
if (!isRequestAllowed(req, port)) {
|
|
898
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
899
|
+
}
|
|
900
|
+
return withSecurityHeaders(
|
|
901
|
+
await handleSessionsAddSource(ctxHolder, store, req, { server }),
|
|
902
|
+
isDev
|
|
903
|
+
);
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
"/api/sessions/sources/:id": {
|
|
907
|
+
DELETE: async (req: Request, server: RequestPeerServer) => {
|
|
908
|
+
if (!isRequestAllowed(req, port)) {
|
|
909
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
910
|
+
}
|
|
911
|
+
const id = decodeURIComponent(
|
|
912
|
+
new URL(req.url).pathname.split("/")[4] ?? ""
|
|
913
|
+
);
|
|
914
|
+
return withSecurityHeaders(
|
|
915
|
+
await handleSessionsRemoveSource(ctxHolder, store, id, req, {
|
|
916
|
+
server,
|
|
917
|
+
}),
|
|
918
|
+
isDev
|
|
919
|
+
);
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
"/api/sessions/automation/run": {
|
|
923
|
+
POST: async (req: Request) => {
|
|
924
|
+
if (!isRequestAllowed(req, port)) {
|
|
925
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
926
|
+
}
|
|
927
|
+
return withSecurityHeaders(
|
|
928
|
+
await handleSessionsAutomationRun(ctxHolder, store, req),
|
|
929
|
+
isDev
|
|
930
|
+
);
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
"/api/sessions/automation/:id": {
|
|
934
|
+
PUT: async (req: Request, server: RequestPeerServer) => {
|
|
935
|
+
if (!isRequestAllowed(req, port)) {
|
|
936
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
937
|
+
}
|
|
938
|
+
return withSecurityHeaders(
|
|
939
|
+
await handleSessionsAutomationSet(
|
|
940
|
+
ctxHolder,
|
|
941
|
+
store,
|
|
942
|
+
automationProfileId(req),
|
|
943
|
+
req,
|
|
944
|
+
{ server }
|
|
945
|
+
),
|
|
946
|
+
isDev
|
|
947
|
+
);
|
|
948
|
+
},
|
|
949
|
+
DELETE: async (req: Request, server: RequestPeerServer) => {
|
|
950
|
+
if (!isRequestAllowed(req, port)) {
|
|
951
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
952
|
+
}
|
|
953
|
+
return withSecurityHeaders(
|
|
954
|
+
await handleSessionsAutomationRemove(
|
|
955
|
+
ctxHolder,
|
|
956
|
+
store,
|
|
957
|
+
automationProfileId(req),
|
|
958
|
+
req,
|
|
959
|
+
{ server }
|
|
960
|
+
),
|
|
961
|
+
isDev
|
|
962
|
+
);
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
"/api/sessions/automation/:id/preview": {
|
|
966
|
+
GET: async (req: Request, server: RequestPeerServer) =>
|
|
967
|
+
!isOriginOrTokenAllowed(req, port)
|
|
968
|
+
? withSecurityHeaders(forbiddenResponse(), isDev)
|
|
969
|
+
: withSecurityHeaders(
|
|
970
|
+
await handleResidentRead(
|
|
971
|
+
runtime as ResidentRuntime,
|
|
972
|
+
req,
|
|
973
|
+
() =>
|
|
974
|
+
handleSessionsAutomationPreview(
|
|
975
|
+
ctxHolder,
|
|
976
|
+
automationProfileId(req),
|
|
977
|
+
req,
|
|
978
|
+
{ server }
|
|
979
|
+
)
|
|
980
|
+
),
|
|
981
|
+
isDev
|
|
982
|
+
),
|
|
983
|
+
},
|
|
984
|
+
"/api/sessions/automation/:id/enable": {
|
|
985
|
+
POST: async (req: Request, server: RequestPeerServer) => {
|
|
986
|
+
if (!isRequestAllowed(req, port)) {
|
|
987
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
988
|
+
}
|
|
989
|
+
return withSecurityHeaders(
|
|
990
|
+
await handleSessionsAutomationEnable(
|
|
991
|
+
ctxHolder,
|
|
992
|
+
store,
|
|
993
|
+
automationProfileId(req),
|
|
994
|
+
req,
|
|
995
|
+
{ server }
|
|
996
|
+
),
|
|
997
|
+
isDev
|
|
998
|
+
);
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
"/api/sessions/automation/:id/disable": {
|
|
1002
|
+
POST: async (req: Request, server: RequestPeerServer) => {
|
|
1003
|
+
if (!isRequestAllowed(req, port)) {
|
|
1004
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1005
|
+
}
|
|
1006
|
+
return withSecurityHeaders(
|
|
1007
|
+
await handleSessionsAutomationDisable(
|
|
1008
|
+
ctxHolder,
|
|
1009
|
+
store,
|
|
1010
|
+
automationProfileId(req),
|
|
1011
|
+
req,
|
|
1012
|
+
{ server }
|
|
1013
|
+
),
|
|
1014
|
+
isDev
|
|
1015
|
+
);
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
"/api/sessions/init": {
|
|
1019
|
+
POST: async (req: Request, server: RequestPeerServer) => {
|
|
1020
|
+
if (!isRequestAllowed(req, port)) {
|
|
1021
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1022
|
+
}
|
|
1023
|
+
return withSecurityHeaders(
|
|
1024
|
+
await handleSessionsInit(ctxHolder, store, req, { server }),
|
|
1025
|
+
isDev
|
|
1026
|
+
);
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
836
1029
|
"/api/memory/remember": {
|
|
837
1030
|
POST: async (req: Request) => {
|
|
838
1031
|
if (!isRequestAllowed(req, port)) {
|
|
@@ -1046,6 +1239,18 @@ export async function startServer(
|
|
|
1046
1239
|
);
|
|
1047
1240
|
},
|
|
1048
1241
|
},
|
|
1242
|
+
"/api/requests/:requestId": {
|
|
1243
|
+
GET: async (req: Request) => {
|
|
1244
|
+
const url = new URL(req.url);
|
|
1245
|
+
const requestId = decodeURIComponent(
|
|
1246
|
+
url.pathname.split("/").pop() || ""
|
|
1247
|
+
);
|
|
1248
|
+
return withSecurityHeaders(
|
|
1249
|
+
await handleRequestStatus(store, requestId),
|
|
1250
|
+
isDev
|
|
1251
|
+
);
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1049
1254
|
"/api/doc": {
|
|
1050
1255
|
GET: async (req: Request) => {
|
|
1051
1256
|
const url = new URL(req.url);
|
|
@@ -1539,7 +1744,7 @@ export async function startServer(
|
|
|
1539
1744
|
);
|
|
1540
1745
|
},
|
|
1541
1746
|
},
|
|
1542
|
-
},
|
|
1747
|
+
}),
|
|
1543
1748
|
// Production catch-all: /vendor/pdfjs prefix, then hashed SPA chunks
|
|
1544
1749
|
// (gzip + immutable) and the private SPA source — the same factory the
|
|
1545
1750
|
// tests mount (no test-only fallback path).
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-only session automation tick: heartbeat, coalesced schedule
|
|
3
|
+
* admissions and a drain of pending profiles through the manual importer.
|
|
4
|
+
*
|
|
5
|
+
* Runs only in `gno daemon` on a session-archive config (never in `serve`).
|
|
6
|
+
* One tick at a time over the daemon's background-work tracker; the owner
|
|
7
|
+
* config is reread every tick, so enable/disable take effect without a
|
|
8
|
+
* restart. Imports take the shared write lease; a busy lease backs off.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SessionAutomationRunResult } from "../sessions/types";
|
|
12
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
13
|
+
|
|
14
|
+
import { acquireCliWriteLease } from "../core/write-lease";
|
|
15
|
+
import { heartbeatAutomation, tickAutomation } from "../sessions/automation";
|
|
16
|
+
import { AUTOMATION_TICK_MS } from "../sessions/automation-state";
|
|
17
|
+
|
|
18
|
+
const LEASE_HOLDER_COMMAND = "gno daemon (session automation)";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The importer syncs the lexical index itself, so the resident watcher later
|
|
22
|
+
* sees unchanged files and never queues embedding: do it here for the
|
|
23
|
+
* collections a run synced, and mark the mutation (as the REST import does).
|
|
24
|
+
*/
|
|
25
|
+
export function notifyAutomationImport(
|
|
26
|
+
result: SessionAutomationRunResult,
|
|
27
|
+
sink: {
|
|
28
|
+
markMutation: () => void;
|
|
29
|
+
notifySyncComplete: (collections: string[]) => void;
|
|
30
|
+
}
|
|
31
|
+
): void {
|
|
32
|
+
const collections = [
|
|
33
|
+
...new Set(
|
|
34
|
+
result.receipts.flatMap((receipt) => receipt.lexical.collections)
|
|
35
|
+
),
|
|
36
|
+
];
|
|
37
|
+
if (collections.length === 0) return;
|
|
38
|
+
sink.markMutation();
|
|
39
|
+
sink.notifySyncComplete(collections);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SessionAutomationSchedulerOptions {
|
|
43
|
+
store: SqliteAdapter;
|
|
44
|
+
configPath: string;
|
|
45
|
+
indexName: string;
|
|
46
|
+
dbPath: string;
|
|
47
|
+
startBackgroundWork: (
|
|
48
|
+
operation: (signal: AbortSignal) => Promise<void>
|
|
49
|
+
) => boolean;
|
|
50
|
+
/** Called after every run; the daemon logs content-free counts. */
|
|
51
|
+
onResult?: (result: SessionAutomationRunResult) => void;
|
|
52
|
+
onError?: (error: unknown) => void;
|
|
53
|
+
now?: () => Date;
|
|
54
|
+
tickMs?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class SessionAutomationScheduler {
|
|
58
|
+
readonly #options: SessionAutomationSchedulerOptions;
|
|
59
|
+
readonly #startedAt: Date;
|
|
60
|
+
#timer: ReturnType<typeof setTimeout> | null = null;
|
|
61
|
+
#heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
62
|
+
#running: Promise<void> | null = null;
|
|
63
|
+
#disposed = false;
|
|
64
|
+
|
|
65
|
+
constructor(options: SessionAutomationSchedulerOptions) {
|
|
66
|
+
this.#options = options;
|
|
67
|
+
this.#startedAt = (options.now ?? (() => new Date()))();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Drain once at startup, then tick; heartbeat independently of ticks. */
|
|
71
|
+
start(): void {
|
|
72
|
+
this.#beat();
|
|
73
|
+
this.#heartbeat = setInterval(
|
|
74
|
+
() => this.#beat(),
|
|
75
|
+
this.#options.tickMs ?? AUTOMATION_TICK_MS
|
|
76
|
+
);
|
|
77
|
+
this.#heartbeat.unref?.();
|
|
78
|
+
this.#schedule(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A tick or import in progress must not let the heartbeat go stale. */
|
|
82
|
+
#beat(): void {
|
|
83
|
+
if (this.#disposed) return;
|
|
84
|
+
const options = this.#options;
|
|
85
|
+
heartbeatAutomation({
|
|
86
|
+
configPath: options.configPath,
|
|
87
|
+
indexName: options.indexName,
|
|
88
|
+
now: options.now,
|
|
89
|
+
daemonStartedAt: this.#startedAt,
|
|
90
|
+
}).catch((error: unknown) => options.onError?.(error));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One tick; coalesces with a tick already in flight. */
|
|
94
|
+
tick(): Promise<void> {
|
|
95
|
+
if (this.#running) return this.#running;
|
|
96
|
+
const options = this.#options;
|
|
97
|
+
const run = tickAutomation({
|
|
98
|
+
configPath: options.configPath,
|
|
99
|
+
indexName: options.indexName,
|
|
100
|
+
store: options.store,
|
|
101
|
+
now: options.now,
|
|
102
|
+
daemonStartedAt: this.#startedAt,
|
|
103
|
+
// The daemon serves MCP and status over HTTP: keep its loop free.
|
|
104
|
+
inChildProcess: true,
|
|
105
|
+
acquireLease: async () => {
|
|
106
|
+
const lease = await acquireCliWriteLease({
|
|
107
|
+
dbPath: options.dbPath,
|
|
108
|
+
waitMs: 0,
|
|
109
|
+
noWait: true,
|
|
110
|
+
command: LEASE_HOLDER_COMMAND,
|
|
111
|
+
});
|
|
112
|
+
return lease.ok ? { ok: true, release: lease.release } : { ok: false };
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
.then((results) => {
|
|
116
|
+
for (const result of results) options.onResult?.(result);
|
|
117
|
+
})
|
|
118
|
+
.catch((error: unknown) => options.onError?.(error))
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.#running = null;
|
|
121
|
+
this.#schedule(options.tickMs ?? AUTOMATION_TICK_MS);
|
|
122
|
+
});
|
|
123
|
+
this.#running = run;
|
|
124
|
+
return run;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
dispose(): void {
|
|
128
|
+
this.#disposed = true;
|
|
129
|
+
if (this.#heartbeat) clearInterval(this.#heartbeat);
|
|
130
|
+
this.#heartbeat = null;
|
|
131
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
132
|
+
this.#timer = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#schedule(delayMs: number): void {
|
|
136
|
+
if (this.#disposed) return;
|
|
137
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
138
|
+
this.#timer = setTimeout(() => {
|
|
139
|
+
this.#timer = null;
|
|
140
|
+
if (this.#disposed || this.#running) return;
|
|
141
|
+
const started = this.#options.startBackgroundWork(() => this.tick());
|
|
142
|
+
if (!started) this.#schedule(this.#options.tickMs ?? AUTOMATION_TICK_MS);
|
|
143
|
+
}, delayMs);
|
|
144
|
+
this.#timer.unref?.();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ContentTypeBoostStatus } from "../config/content-types";
|
|
2
2
|
import type { ActivationStatus } from "../core/activation-status";
|
|
3
3
|
import type { ChunkingStatus } from "../store/chunking";
|
|
4
|
+
import type {
|
|
5
|
+
VectorPartitionStatus,
|
|
6
|
+
VectorRuntimeStatus,
|
|
7
|
+
} from "../store/vector/status";
|
|
4
8
|
|
|
5
9
|
export type HealthCheckStatus = "ok" | "warn" | "error";
|
|
6
10
|
|
|
@@ -109,6 +113,16 @@ export interface ResidentStatus {
|
|
|
109
113
|
content: number;
|
|
110
114
|
index: number;
|
|
111
115
|
};
|
|
116
|
+
/** Present only while a background job is in trouble. */
|
|
117
|
+
backgroundIssues?: BackgroundIssue[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A resident background job that keeps failing, has stopped retrying, or overruns. */
|
|
121
|
+
export interface BackgroundIssue {
|
|
122
|
+
job: "embed" | "resident";
|
|
123
|
+
state: "failing" | "parked" | "overrunning" | "unresponsive";
|
|
124
|
+
consecutiveFailures: number;
|
|
125
|
+
runningSeconds: number | null;
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
export interface BackgroundServiceState {
|
|
@@ -186,6 +200,8 @@ export interface AppStatusResponse {
|
|
|
186
200
|
totalDocuments: number;
|
|
187
201
|
totalChunks: number;
|
|
188
202
|
embeddingBacklog: number;
|
|
203
|
+
vectorPartitions?: VectorPartitionStatus[];
|
|
204
|
+
vectorRuntime?: VectorRuntimeStatus;
|
|
189
205
|
lastUpdated: string | null;
|
|
190
206
|
recentErrors: number;
|
|
191
207
|
healthy: boolean;
|
package/src/serve/status.ts
CHANGED
|
@@ -734,6 +734,8 @@ export async function buildAppStatus(
|
|
|
734
734
|
totalDocuments: status.activeDocuments,
|
|
735
735
|
totalChunks: status.totalChunks,
|
|
736
736
|
embeddingBacklog: status.embeddingBacklog,
|
|
737
|
+
vectorPartitions: status.vectorPartitions,
|
|
738
|
+
vectorRuntime: status.vectorRuntime,
|
|
737
739
|
chunking: status.chunking,
|
|
738
740
|
lastUpdated: status.lastUpdatedAt,
|
|
739
741
|
recentErrors: status.recentErrors,
|
|
@@ -45,6 +45,9 @@ export const WATCHER_MAX_SUPPRESSION_ENTRIES = 4_096;
|
|
|
45
45
|
/** Bounded retry delay after failed classification/sync. */
|
|
46
46
|
export const WATCHER_RETRY_BACKOFF_MS = 500;
|
|
47
47
|
|
|
48
|
+
/** Retry delay while another writer holds the shared writer lease. */
|
|
49
|
+
export const WATCHER_LEASE_RETRY_MS = 5_000;
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* Single fixed budget for fallback classification across visited directories,
|
|
50
53
|
* candidates, removals, dirty dirs, and aggregate store rows.
|
|
@@ -219,7 +219,8 @@ export function requeueAfterFailure(
|
|
|
219
219
|
collectionName: string,
|
|
220
220
|
exact: string[],
|
|
221
221
|
dirty: string[],
|
|
222
|
-
forceFlags?: PendingForceFlags
|
|
222
|
+
forceFlags?: PendingForceFlags,
|
|
223
|
+
delayMs = WATCHER_RETRY_BACKOFF_MS
|
|
223
224
|
): void {
|
|
224
225
|
queueWithoutSchedule(host, collectionName, exact, dirty, forceFlags);
|
|
225
226
|
if (host.disposed()) {
|
|
@@ -241,7 +242,7 @@ export function requeueAfterFailure(
|
|
|
241
242
|
host.timers.delete(collectionName);
|
|
242
243
|
host.retryScheduled.delete(collectionName);
|
|
243
244
|
startFlush(host, collectionName);
|
|
244
|
-
},
|
|
245
|
+
}, delayMs)
|
|
245
246
|
);
|
|
246
247
|
}
|
|
247
248
|
|
|
@@ -14,6 +14,7 @@ import type { WatchQueueHost } from "./watch-service-events";
|
|
|
14
14
|
import type { CollectionPending } from "./watch-service-state";
|
|
15
15
|
import type { WatcherSnapshot, WatcherSnapshotFs } from "./watch-snapshot";
|
|
16
16
|
|
|
17
|
+
import { WATCHER_LEASE_RETRY_MS } from "./watch-reconciliation-shared";
|
|
17
18
|
import {
|
|
18
19
|
requeueAfterFailure,
|
|
19
20
|
requeueGenerationReconcile,
|
|
@@ -65,6 +66,11 @@ export interface RunFlushContext {
|
|
|
65
66
|
snapshotFs?: WatcherSnapshotFs;
|
|
66
67
|
/** Test seam: lower snapshot entry ceiling for overflow→full proofs. */
|
|
67
68
|
snapshotEntryCeiling?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Shared writer lease for this flush; null when another writer holds it,
|
|
71
|
+
* which leaves the work queued for the retry timer instead of waiting.
|
|
72
|
+
*/
|
|
73
|
+
acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/**
|
|
@@ -86,19 +92,44 @@ export async function runOwnedCollectionFlush(
|
|
|
86
92
|
return;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
ctx.syncing.add(collectionName);
|
|
96
|
+
let releaseLease: (() => Promise<void>) | null = null;
|
|
97
|
+
if (ctx.acquireWriteLease) {
|
|
98
|
+
releaseLease = await ctx.acquireWriteLease();
|
|
99
|
+
if (!releaseLease || ctx.disposed()) {
|
|
100
|
+
ctx.syncing.delete(collectionName);
|
|
101
|
+
await releaseLease?.();
|
|
102
|
+
if (!ctx.disposed()) {
|
|
103
|
+
requeueAfterFailure(
|
|
104
|
+
ctx.queueHost,
|
|
105
|
+
collectionName,
|
|
106
|
+
[],
|
|
107
|
+
[],
|
|
108
|
+
undefined,
|
|
109
|
+
WATCHER_LEASE_RETRY_MS
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
const collection = ctx
|
|
90
117
|
.collections()
|
|
91
118
|
.find((entry) => entry.name === collectionName);
|
|
92
119
|
if (!collection) {
|
|
120
|
+
await releaseLease?.();
|
|
121
|
+
ctx.syncing.delete(collectionName);
|
|
93
122
|
ctx.pendingByCollection.delete(collectionName);
|
|
94
123
|
ctx.flushDeadlineAt.delete(collectionName);
|
|
95
124
|
return;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
|
|
127
|
+
// Events that arrived while the lease was being taken join this flush.
|
|
128
|
+
const taken = takePending(
|
|
129
|
+
ctx.pendingByCollection.get(collectionName) ?? pending
|
|
130
|
+
);
|
|
99
131
|
ctx.pendingByCollection.set(collectionName, emptyPending());
|
|
100
132
|
ctx.flushDeadlineAt.delete(collectionName);
|
|
101
|
-
ctx.syncing.add(collectionName);
|
|
102
133
|
|
|
103
134
|
const ownerGeneration = ctx.collectionGenerations.get(collectionName) ?? 0;
|
|
104
135
|
const ownerRoot = normalize(collection.path);
|
|
@@ -219,6 +250,8 @@ export async function runOwnedCollectionFlush(
|
|
|
219
250
|
throw outcome.error;
|
|
220
251
|
}
|
|
221
252
|
} finally {
|
|
253
|
+
// Release before any follow-up flush below tries to take the lease again.
|
|
254
|
+
await releaseLease?.();
|
|
222
255
|
ctx.syncing.delete(collectionName);
|
|
223
256
|
ctx.clearLifecycleTombstones(collectionName);
|
|
224
257
|
ctx.pruneSuppression();
|
|
@@ -108,6 +108,8 @@ interface CollectionWatchServiceOptions {
|
|
|
108
108
|
* Production leaves this unset (uses WATCHER_SNAPSHOT_ENTRY_CEILING).
|
|
109
109
|
*/
|
|
110
110
|
snapshotEntryCeiling?: number;
|
|
111
|
+
/** Shared writer lease taken (no wait) around each flush's writes. */
|
|
112
|
+
acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
export class CollectionWatchService {
|
|
@@ -146,6 +148,7 @@ export class CollectionWatchService {
|
|
|
146
148
|
| undefined;
|
|
147
149
|
readonly #snapshotFs: WatcherSnapshotFs | undefined;
|
|
148
150
|
readonly #snapshotEntryCeiling: number | undefined;
|
|
151
|
+
readonly #acquireWriteLease: CollectionWatchServiceOptions["acquireWriteLease"];
|
|
149
152
|
#nextCollectionGeneration = 0;
|
|
150
153
|
#disposed = false;
|
|
151
154
|
#lastEventAt: string | null = null;
|
|
@@ -169,6 +172,7 @@ export class CollectionWatchService {
|
|
|
169
172
|
this.#buildSnapshot = options.buildSnapshot;
|
|
170
173
|
this.#snapshotFs = options.snapshotFs;
|
|
171
174
|
this.#snapshotEntryCeiling = options.snapshotEntryCeiling;
|
|
175
|
+
this.#acquireWriteLease = options.acquireWriteLease;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
start(): void {
|
|
@@ -394,6 +398,7 @@ export class CollectionWatchService {
|
|
|
394
398
|
},
|
|
395
399
|
snapshotFs: this.#snapshotFs,
|
|
396
400
|
snapshotEntryCeiling: this.#snapshotEntryCeiling,
|
|
401
|
+
acquireWriteLease: this.#acquireWriteLease,
|
|
397
402
|
});
|
|
398
403
|
}
|
|
399
404
|
|