@wenathlan/devthink 2.0.15 → 2.0.19
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/CHANGELOG.md +45 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/session.ts
DELETED
|
@@ -1,1162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session module of the 1.1.90 consolidation: every correlated variation of the session logic interned in this one file, so the module family carries one surface without duplicate variations.
|
|
3
|
-
* The correlation is the session as the persistent interface: sessions holds the stored state of the 1.1.49 memory family (task state checkpoints with their checksums, session capture with restore plans, naming, diffing, search, versioned export and import, retention expiry and the crash interrupted mark); sessioninterface turns the same session into the primary interface of the 1.1.63 family (sitenotes sealed at rest when sensitive, the append only scratchpad, runsummary distillation, semanticrecall with its fingerprint index, correction and consent memory as advisory history, the sessiongrid derived from the existing stores with no new state, historysearch over one corpus, error surfaces with retry hints and cancelrun with its queued only rollback).
|
|
4
|
-
* No window, retention, limit or rollback scope is ever hardcoded: every length and expiry stays the user's choice with no engine cap, and no interface surface ever bypasses the human review.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/* ── Merged from sessions.ts ── */
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
autointerval,
|
|
11
|
-
restoreplan,
|
|
12
|
-
searchfield,
|
|
13
|
-
searchquery,
|
|
14
|
-
sessionchange,
|
|
15
|
-
sessiondiff,
|
|
16
|
-
sessionfile,
|
|
17
|
-
sessionfolder,
|
|
18
|
-
sessionmatch,
|
|
19
|
-
sessionrecord,
|
|
20
|
-
sessiontab,
|
|
21
|
-
snapshotscope,
|
|
22
|
-
snapshotplan,
|
|
23
|
-
stepoutcome,
|
|
24
|
-
taskstate,
|
|
25
|
-
} from "./types.js";
|
|
26
|
-
|
|
27
|
-
/** The session kinds of the 1.1.49 memory family: task state persistence, session capture, restore, naming, diffing, search, export and import. */
|
|
28
|
-
export const sessionkinds: string[] = [
|
|
29
|
-
"persiststate",
|
|
30
|
-
"capturesession",
|
|
31
|
-
"restoresession",
|
|
32
|
-
"namedsessions",
|
|
33
|
-
"diffsessions",
|
|
34
|
-
"searchsessions",
|
|
35
|
-
"exportsessions",
|
|
36
|
-
"importsessions",
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
/** The format version of exported session files; imports of unknown versions are refused. */
|
|
40
|
-
export const sessionfileversion = 1;
|
|
41
|
-
|
|
42
|
-
/** The reviewed snapshot section toggles: tabs, scroll positions, form state, local storage and cookies. */
|
|
43
|
-
export const snapshotsections: snapshotscope[] = ["tabs", "scroll", "forms", "storage", "cookies"];
|
|
44
|
-
|
|
45
|
-
/** The searchable fields of a session search: urls, titles, record names and captured text. */
|
|
46
|
-
export const searchfields: searchfield[] = ["urls", "titles", "names", "text"];
|
|
47
|
-
|
|
48
|
-
/** Stable local checksum of one string payload so persisted state and exported files detect corruption without a network dependency. */
|
|
49
|
-
function checksumtext(payload: string): string {
|
|
50
|
-
let hash = 0x811c9dc5;
|
|
51
|
-
for (let index = 0; index < payload.length; index += 1) {
|
|
52
|
-
hash ^= payload.charCodeAt(index);
|
|
53
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
54
|
-
}
|
|
55
|
-
return hash.toString(16).padStart(8, "0");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Checksums the run id, step cursor and recorded outputs of a task state so corruption is detected before a resume. */
|
|
59
|
-
export function taskstatechecksum(runid: string, stepcursor: number, outputs: stepoutcome[]): string {
|
|
60
|
-
return checksumtext(
|
|
61
|
-
`${runid}:${stepcursor}:${outputs.length}:${outputs.map((output) => `${output.stepid}:${output.ok}:${output.summary.length}`).join("|")}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Builds one persisted task state checkpoint of a run with its checksum. */
|
|
66
|
-
export function taskstateof(input: {
|
|
67
|
-
runid: string;
|
|
68
|
-
stepcursor: number;
|
|
69
|
-
outputs: stepoutcome[];
|
|
70
|
-
checkpointat: number;
|
|
71
|
-
}): taskstate {
|
|
72
|
-
return {
|
|
73
|
-
runid: input.runid,
|
|
74
|
-
stepcursor: input.stepcursor,
|
|
75
|
-
outputs: input.outputs,
|
|
76
|
-
checkpointat: input.checkpointat,
|
|
77
|
-
checksum: taskstatechecksum(input.runid, input.stepcursor, input.outputs),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** True when a persisted task state carries the intact checksum of its own payload; a corrupted checkpoint never resumes. */
|
|
82
|
-
export function taskstatevalid(state: taskstate | undefined): boolean {
|
|
83
|
-
if (!state || typeof state.runid !== "string" || !state.runid.trim()) return false;
|
|
84
|
-
if (typeof state.stepcursor !== "number" || !Number.isInteger(state.stepcursor) || state.stepcursor < 0) return false;
|
|
85
|
-
if (typeof state.checkpointat !== "number" || !Number.isFinite(state.checkpointat)) return false;
|
|
86
|
-
if (!Array.isArray(state.outputs)) return false;
|
|
87
|
-
return state.checksum === taskstatechecksum(state.runid, state.stepcursor, state.outputs);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Normalizes one captured tab of a saved session: url, title, index, scroll position and form state. */
|
|
91
|
-
export function sessiontabof(value: unknown): sessiontab | undefined {
|
|
92
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
93
|
-
const candidate = value as Record<string, unknown>;
|
|
94
|
-
if (typeof candidate.url !== "string" || !candidate.url.trim()) return undefined;
|
|
95
|
-
if (typeof candidate.title !== "string") return undefined;
|
|
96
|
-
if (typeof candidate.index !== "number" || !Number.isInteger(candidate.index) || candidate.index < 0)
|
|
97
|
-
return undefined;
|
|
98
|
-
const scrollx = typeof candidate.scrollx === "number" && Number.isFinite(candidate.scrollx) ? candidate.scrollx : 0;
|
|
99
|
-
const scrolly = typeof candidate.scrolly === "number" && Number.isFinite(candidate.scrolly) ? candidate.scrolly : 0;
|
|
100
|
-
const forms = Array.isArray(candidate.forms)
|
|
101
|
-
? candidate.forms.flatMap((entry) => {
|
|
102
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
103
|
-
const form = entry as Record<string, unknown>;
|
|
104
|
-
if (typeof form.selector !== "string" || !form.selector.trim()) return [];
|
|
105
|
-
return [{ selector: form.selector, value: typeof form.value === "string" ? form.value : "" }];
|
|
106
|
-
})
|
|
107
|
-
: [];
|
|
108
|
-
return { url: candidate.url, title: candidate.title, index: candidate.index, scrollx, scrolly, forms };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Normalizes one reviewed auto snapshot interval: a positive period, a positive maximum snapshot count and a zero or positive expiry window; every value stays a user choice with no code ceiling. */
|
|
112
|
-
export function autointervalof(value: unknown): autointerval | undefined {
|
|
113
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
114
|
-
const candidate = value as Record<string, unknown>;
|
|
115
|
-
if (typeof candidate.period !== "number" || !Number.isFinite(candidate.period) || candidate.period <= 0)
|
|
116
|
-
return undefined;
|
|
117
|
-
if (
|
|
118
|
-
typeof candidate.maxsnapshots !== "number" ||
|
|
119
|
-
!Number.isInteger(candidate.maxsnapshots) ||
|
|
120
|
-
candidate.maxsnapshots < 1
|
|
121
|
-
)
|
|
122
|
-
return undefined;
|
|
123
|
-
if (typeof candidate.expiry !== "number" || !Number.isFinite(candidate.expiry) || candidate.expiry < 0)
|
|
124
|
-
return undefined;
|
|
125
|
-
return { period: candidate.period, maxsnapshots: candidate.maxsnapshots, expiry: candidate.expiry };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Normalizes one reviewed snapshot plan: the scope, the section toggles of the reviewed grammar, the capture link flag and the optional auto interval. */
|
|
129
|
-
export function snapshotplanof(value: unknown): snapshotplan | undefined {
|
|
130
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
131
|
-
const candidate = value as Record<string, unknown>;
|
|
132
|
-
if (candidate.scope !== "tab" && candidate.scope !== "run" && candidate.scope !== "all") return undefined;
|
|
133
|
-
const sections = Array.isArray(candidate.sections)
|
|
134
|
-
? candidate.sections.flatMap((section) =>
|
|
135
|
-
typeof section === "string" && snapshotsections.includes(section as snapshotscope)
|
|
136
|
-
? [section as snapshotscope]
|
|
137
|
-
: [],
|
|
138
|
-
)
|
|
139
|
-
: [];
|
|
140
|
-
if (sections.length === 0) return undefined;
|
|
141
|
-
if (typeof candidate.captures !== "boolean") return undefined;
|
|
142
|
-
const auto = candidate.auto === undefined ? undefined : autointervalof(candidate.auto);
|
|
143
|
-
if (candidate.auto !== undefined && auto === undefined) return undefined;
|
|
144
|
-
return { scope: candidate.scope, sections, captures: candidate.captures, ...(auto !== undefined ? { auto } : {}) };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Normalizes one reviewed restore plan: the tab, form and capture policies of the reviewed grammar. */
|
|
148
|
-
export function restoreplanof(value: unknown): restoreplan | undefined {
|
|
149
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
150
|
-
const candidate = value as Record<string, unknown>;
|
|
151
|
-
if (candidate.tabpolicy !== "reopen" && candidate.tabpolicy !== "skip") return undefined;
|
|
152
|
-
if (candidate.formpolicy !== "restore" && candidate.formpolicy !== "skip") return undefined;
|
|
153
|
-
if (candidate.capturepolicy !== "link" && candidate.capturepolicy !== "skip") return undefined;
|
|
154
|
-
return { tabpolicy: candidate.tabpolicy, formpolicy: candidate.formpolicy, capturepolicy: candidate.capturepolicy };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Normalizes one reviewed session search query: the non-empty term list, the searched fields of the reviewed grammar and the optional time window. */
|
|
158
|
-
export function searchqueryof(value: unknown): searchquery | undefined {
|
|
159
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
160
|
-
const candidate = value as Record<string, unknown>;
|
|
161
|
-
const terms = Array.isArray(candidate.terms)
|
|
162
|
-
? candidate.terms.flatMap((term) => (typeof term === "string" && term.trim() ? [term.trim()] : []))
|
|
163
|
-
: [];
|
|
164
|
-
if (terms.length === 0) return undefined;
|
|
165
|
-
const fields = Array.isArray(candidate.fields)
|
|
166
|
-
? candidate.fields.flatMap((field) =>
|
|
167
|
-
typeof field === "string" && searchfields.includes(field as searchfield) ? [field as searchfield] : [],
|
|
168
|
-
)
|
|
169
|
-
: [...searchfields];
|
|
170
|
-
if (fields.length === 0) return undefined;
|
|
171
|
-
const from = typeof candidate.from === "number" && Number.isFinite(candidate.from) ? candidate.from : undefined;
|
|
172
|
-
const to = typeof candidate.to === "number" && Number.isFinite(candidate.to) ? candidate.to : undefined;
|
|
173
|
-
if (from !== undefined && to !== undefined && from > to) return undefined;
|
|
174
|
-
return { terms, fields, ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}) };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Normalizes one session folder: the name, the optional parent folder and the tag list. */
|
|
178
|
-
export function sessionfolderof(value: unknown): sessionfolder | undefined {
|
|
179
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
180
|
-
const candidate = value as Record<string, unknown>;
|
|
181
|
-
if (typeof candidate.name !== "string" || !candidate.name.trim()) return undefined;
|
|
182
|
-
const parent = typeof candidate.parent === "string" && candidate.parent.trim() ? candidate.parent : undefined;
|
|
183
|
-
const tags = Array.isArray(candidate.tags)
|
|
184
|
-
? candidate.tags.flatMap((tag) => (typeof tag === "string" && tag.trim() ? [tag] : []))
|
|
185
|
-
: [];
|
|
186
|
-
return { name: candidate.name, ...(parent !== undefined ? { parent } : {}), tags };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Builds one saved session record with its default tag list and capture linkage. */
|
|
190
|
-
export function newsessionrecord(input: {
|
|
191
|
-
id: string;
|
|
192
|
-
name: string;
|
|
193
|
-
createdat: number;
|
|
194
|
-
tabs: sessiontab[];
|
|
195
|
-
captures: string[];
|
|
196
|
-
storage: Array<{ origin: string; keys: string[]; values: string[] }>;
|
|
197
|
-
cookies: Array<{ origin: string; names: string[] }>;
|
|
198
|
-
auto?: boolean;
|
|
199
|
-
folder?: string;
|
|
200
|
-
tags?: string[];
|
|
201
|
-
}): sessionrecord {
|
|
202
|
-
return {
|
|
203
|
-
id: input.id,
|
|
204
|
-
name: input.name,
|
|
205
|
-
createdat: input.createdat,
|
|
206
|
-
tabs: input.tabs,
|
|
207
|
-
captures: input.captures,
|
|
208
|
-
storage: input.storage,
|
|
209
|
-
cookies: input.cookies,
|
|
210
|
-
...(input.folder !== undefined ? { folder: input.folder } : {}),
|
|
211
|
-
tags: input.tags ?? [],
|
|
212
|
-
...(input.auto === true ? { auto: true } : {}),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Classifies the tab, url, form and storage changes between two saved sessions by matching tabs on their recorded index. */
|
|
217
|
-
export function diffsessionrecords(left: sessionrecord, right: sessionrecord): sessionchange[] {
|
|
218
|
-
const changes: sessionchange[] = [];
|
|
219
|
-
const leftbyindex = new Map(left.tabs.map((tab) => [tab.index, tab]));
|
|
220
|
-
const rightbyindex = new Map(right.tabs.map((tab) => [tab.index, tab]));
|
|
221
|
-
for (const tab of right.tabs) {
|
|
222
|
-
const prior = leftbyindex.get(tab.index);
|
|
223
|
-
if (!prior) {
|
|
224
|
-
changes.push({ class: "added", subject: "tab", detail: `Tab ${tab.index} added: ${tab.url}` });
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (prior.url !== tab.url)
|
|
228
|
-
changes.push({
|
|
229
|
-
class: "changed",
|
|
230
|
-
subject: "url",
|
|
231
|
-
detail: `Tab ${tab.index} moved from ${prior.url} to ${tab.url}`,
|
|
232
|
-
});
|
|
233
|
-
if (prior.title !== tab.title)
|
|
234
|
-
changes.push({
|
|
235
|
-
class: "changed",
|
|
236
|
-
subject: "tab",
|
|
237
|
-
detail: `Tab ${tab.index} title changed from "${prior.title}" to "${tab.title}"`,
|
|
238
|
-
});
|
|
239
|
-
const priorforms = new Map(prior.forms.map((form) => [form.selector, form.value]));
|
|
240
|
-
for (const form of tab.forms) {
|
|
241
|
-
const before = priorforms.get(form.selector);
|
|
242
|
-
if (before === undefined) {
|
|
243
|
-
changes.push({
|
|
244
|
-
class: "added",
|
|
245
|
-
subject: "form",
|
|
246
|
-
detail: `Form field ${form.selector} of tab ${tab.index} added with a value`,
|
|
247
|
-
});
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (before !== form.value)
|
|
251
|
-
changes.push({
|
|
252
|
-
class: "changed",
|
|
253
|
-
subject: "form",
|
|
254
|
-
detail: `Form field ${form.selector} of tab ${tab.index} changed its captured value`,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
for (const form of prior.forms)
|
|
258
|
-
if (!tab.forms.some((entry) => entry.selector === form.selector))
|
|
259
|
-
changes.push({
|
|
260
|
-
class: "removed",
|
|
261
|
-
subject: "form",
|
|
262
|
-
detail: `Form field ${form.selector} of tab ${tab.index} removed`,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
for (const tab of left.tabs)
|
|
266
|
-
if (!rightbyindex.has(tab.index))
|
|
267
|
-
changes.push({ class: "removed", subject: "tab", detail: `Tab ${tab.index} removed: ${tab.url}` });
|
|
268
|
-
const leftstorage = new Map(left.storage.map((entry) => [entry.origin, entry]));
|
|
269
|
-
for (const entry of right.storage) {
|
|
270
|
-
const prior = leftstorage.get(entry.origin);
|
|
271
|
-
if (!prior) {
|
|
272
|
-
changes.push({
|
|
273
|
-
class: "added",
|
|
274
|
-
subject: "storage",
|
|
275
|
-
detail: `Local storage of ${entry.origin} captured with ${entry.keys.length} keys`,
|
|
276
|
-
});
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (prior.keys.join("|") !== entry.keys.join("|") || prior.values.join("|") !== entry.values.join("|"))
|
|
280
|
-
changes.push({
|
|
281
|
-
class: "changed",
|
|
282
|
-
subject: "storage",
|
|
283
|
-
detail: `Local storage of ${entry.origin} changed its captured keys or values`,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
for (const entry of left.storage)
|
|
287
|
-
if (!right.storage.some((candidate) => candidate.origin === entry.origin))
|
|
288
|
-
changes.push({
|
|
289
|
-
class: "removed",
|
|
290
|
-
subject: "storage",
|
|
291
|
-
detail: `Local storage of ${entry.origin} left the capture`,
|
|
292
|
-
});
|
|
293
|
-
return changes;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** Builds one stored session diff result of two compared records. */
|
|
297
|
-
export function newsessiondiff(input: {
|
|
298
|
-
id: string;
|
|
299
|
-
left: sessionrecord;
|
|
300
|
-
right: sessionrecord;
|
|
301
|
-
at: number;
|
|
302
|
-
}): sessiondiff {
|
|
303
|
-
return {
|
|
304
|
-
id: input.id,
|
|
305
|
-
leftid: input.left.id,
|
|
306
|
-
rightid: input.right.id,
|
|
307
|
-
changes: diffsessionrecords(input.left, input.right),
|
|
308
|
-
at: input.at,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** Searches across saved sessions: matches urls, titles, record names and captured form text inside the reviewed time window and returns every match with its session id. */
|
|
313
|
-
export function searchsessionrecords(query: searchquery, records: sessionrecord[]): sessionmatch[] {
|
|
314
|
-
const matches: sessionmatch[] = [];
|
|
315
|
-
for (const record of records) {
|
|
316
|
-
if (query.from !== undefined && record.createdat < query.from) continue;
|
|
317
|
-
if (query.to !== undefined && record.createdat > query.to) continue;
|
|
318
|
-
const haystacks: Array<{ field: searchfield; text: string }> = [
|
|
319
|
-
{ field: "urls", text: record.tabs.map((tab) => tab.url).join(" ") },
|
|
320
|
-
{ field: "titles", text: record.tabs.map((tab) => tab.title).join(" ") },
|
|
321
|
-
{ field: "names", text: [record.name, record.folder ?? "", ...record.tags].join(" ") },
|
|
322
|
-
{ field: "text", text: record.tabs.flatMap((tab) => tab.forms.map((form) => form.value)).join(" ") },
|
|
323
|
-
];
|
|
324
|
-
for (const haystack of haystacks) {
|
|
325
|
-
if (!query.fields.includes(haystack.field)) continue;
|
|
326
|
-
const lower = haystack.text.toLowerCase();
|
|
327
|
-
for (const term of query.terms) {
|
|
328
|
-
const at = lower.indexOf(term.toLowerCase());
|
|
329
|
-
if (at < 0) continue;
|
|
330
|
-
const start = Math.max(0, at - 30);
|
|
331
|
-
matches.push({
|
|
332
|
-
sessionid: record.id,
|
|
333
|
-
field: haystack.field,
|
|
334
|
-
term,
|
|
335
|
-
at: record.createdat,
|
|
336
|
-
excerpt: haystack.text.slice(start, start + 80).trim(),
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return matches;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** Packs saved session records into one versioned session file with its record ids, byte size and checksum. */
|
|
345
|
-
export function exportsessionfile(records: sessionrecord[], now: number): sessionfile {
|
|
346
|
-
const recordids = records.map((record) => record.id);
|
|
347
|
-
const payload = JSON.stringify(records);
|
|
348
|
-
return {
|
|
349
|
-
formatversion: sessionfileversion,
|
|
350
|
-
records,
|
|
351
|
-
recordids,
|
|
352
|
-
bytesize: payload.length,
|
|
353
|
-
checksum: checksumtext(`${sessionfileversion}:${recordids.join(",")}:${payload.length}`),
|
|
354
|
-
exportedat: now,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** Validates one imported session file: the format version must match, every record must carry intact sections and the checksum must verify before any record review begins. */
|
|
359
|
-
export function importsessionfile(value: unknown): sessionfile | undefined {
|
|
360
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
361
|
-
const candidate = value as Record<string, unknown>;
|
|
362
|
-
if (candidate.formatversion !== sessionfileversion) return undefined;
|
|
363
|
-
const records = Array.isArray(candidate.records)
|
|
364
|
-
? candidate.records.flatMap((record) => (sessionrecordvalid(record) ? [record] : []))
|
|
365
|
-
: [];
|
|
366
|
-
if (records.length === 0) return undefined;
|
|
367
|
-
if (
|
|
368
|
-
!Array.isArray(candidate.recordids) ||
|
|
369
|
-
candidate.recordids.length !== records.length ||
|
|
370
|
-
!candidate.recordids.every((id, index) => id === records[index]?.id)
|
|
371
|
-
)
|
|
372
|
-
return undefined;
|
|
373
|
-
const bytesize =
|
|
374
|
-
typeof candidate.bytesize === "number" && Number.isFinite(candidate.bytesize) ? candidate.bytesize : -1;
|
|
375
|
-
if (bytesize < 0) return undefined;
|
|
376
|
-
const checksum = typeof candidate.checksum === "string" ? candidate.checksum : "";
|
|
377
|
-
if (checksum !== checksumtext(`${sessionfileversion}:${candidate.recordids.join(",")}:${bytesize}`)) return undefined;
|
|
378
|
-
return {
|
|
379
|
-
formatversion: sessionfileversion,
|
|
380
|
-
records,
|
|
381
|
-
recordids: candidate.recordids as string[],
|
|
382
|
-
bytesize,
|
|
383
|
-
checksum,
|
|
384
|
-
exportedat:
|
|
385
|
-
typeof candidate.exportedat === "number" && Number.isFinite(candidate.exportedat) ? candidate.exportedat : 0,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** Structural check of one saved session record so imports never add a broken record to the library. */
|
|
390
|
-
function sessionrecordvalid(value: unknown): value is sessionrecord {
|
|
391
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
392
|
-
const candidate = value as Record<string, unknown>;
|
|
393
|
-
if (typeof candidate.id !== "string" || !candidate.id.trim()) return false;
|
|
394
|
-
if (typeof candidate.name !== "string" || !candidate.name.trim()) return false;
|
|
395
|
-
if (typeof candidate.createdat !== "number" || !Number.isFinite(candidate.createdat)) return false;
|
|
396
|
-
if (!Array.isArray(candidate.tabs) || !candidate.tabs.every((tab) => sessiontabof(tab) !== undefined)) return false;
|
|
397
|
-
if (!Array.isArray(candidate.captures) || !candidate.captures.every((id) => typeof id === "string")) return false;
|
|
398
|
-
if (!Array.isArray(candidate.tags) || !candidate.tags.every((tag) => typeof tag === "string")) return false;
|
|
399
|
-
return true;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/** Expires the heavy sections of saved sessions after the reviewed retention window while the record metadata survives; an absent window keeps every section and no code ceiling applies. */
|
|
403
|
-
export function expiresessions(records: sessionrecord[], retention: number | undefined, now: number): sessionrecord[] {
|
|
404
|
-
if (retention === undefined || !Number.isFinite(retention)) return records;
|
|
405
|
-
return records.map((record) => {
|
|
406
|
-
if (record.sectionsexpired || now - record.createdat < retention) return record;
|
|
407
|
-
return {
|
|
408
|
-
id: record.id,
|
|
409
|
-
name: record.name,
|
|
410
|
-
createdat: record.createdat,
|
|
411
|
-
tabs: [],
|
|
412
|
-
captures: record.captures,
|
|
413
|
-
storage: [],
|
|
414
|
-
cookies: [],
|
|
415
|
-
...(record.folder !== undefined ? { folder: record.folder } : {}),
|
|
416
|
-
tags: record.tags,
|
|
417
|
-
...(record.auto === true ? { auto: true } : {}),
|
|
418
|
-
...(record.restoredat !== undefined ? { restoredat: record.restoredat } : {}),
|
|
419
|
-
sectionsexpired: true,
|
|
420
|
-
};
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/** Lists saved sessions filtered by name substring, folder and time window; the filter stays a user choice with no result cap. */
|
|
425
|
-
export function filteredsessions(
|
|
426
|
-
records: sessionrecord[],
|
|
427
|
-
filter: { name?: string; folder?: string; from?: number; to?: number },
|
|
428
|
-
): sessionrecord[] {
|
|
429
|
-
return records.filter((record) => {
|
|
430
|
-
if (filter.name !== undefined && !record.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
|
|
431
|
-
if (filter.folder !== undefined && record.folder !== filter.folder) return false;
|
|
432
|
-
if (filter.from !== undefined && record.createdat < filter.from) return false;
|
|
433
|
-
if (filter.to !== undefined && record.createdat > filter.to) return false;
|
|
434
|
-
return true;
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/** Marks one persisted task state interrupted by a browser restart when the cursor never reached the end of the plan. */
|
|
439
|
-
export function crashinterrupted(state: taskstate | undefined, plansteps: number, now: number): taskstate | undefined {
|
|
440
|
-
if (!state) return undefined;
|
|
441
|
-
if (state.stepcursor >= plansteps) return state;
|
|
442
|
-
return { ...state, interrupted: true, crashat: state.crashat ?? now };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/* ── Merged from sessioninterface.ts ── */
|
|
446
|
-
|
|
447
|
-
import type {
|
|
448
|
-
agentplan,
|
|
449
|
-
cancelrunaction,
|
|
450
|
-
consentmemoryentry,
|
|
451
|
-
correctionentry,
|
|
452
|
-
emptystatesurface,
|
|
453
|
-
errorsurface,
|
|
454
|
-
errorcause,
|
|
455
|
-
historyindexentry,
|
|
456
|
-
historysearchhit,
|
|
457
|
-
historysearchquery,
|
|
458
|
-
planprogress,
|
|
459
|
-
recallindexentry,
|
|
460
|
-
recallmatch,
|
|
461
|
-
recallquery,
|
|
462
|
-
rollbackdescriptor,
|
|
463
|
-
runsummary,
|
|
464
|
-
scratchpadentry,
|
|
465
|
-
sessiongridrow,
|
|
466
|
-
sitenote,
|
|
467
|
-
storedrunlog,
|
|
468
|
-
tabsessionref,
|
|
469
|
-
} from "./types.js";
|
|
470
|
-
import { randomid } from "./memory.js";
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Session interface logic of the 1.1.63 family.
|
|
474
|
-
* The session becomes the primary interface here: sitenotes keep one record per origin with title, body and author provenance while sensitive bodies seal at rest with a local keystream, the scratchpad holds append only entries per task with step provenance, runsummary distills a completed run into the origins visited, the kinds executed and the per step outcomes inside the user configured window as an offscreen worker task, semanticrecall embeds past extraction records into a fingerprint indexed corpus ranked by text similarity with run and step provenance, correctionmemory captures every plan review edit and rejection per origin and kind, consentmemory keeps every grant, denial, expiry and revocation per origin as advisory history that never auto grants, sessiongrid rows derive from the existing session stores with no new state, historysearch indexes session metadata, notes and summaries into one incremental corpus with matched term highlighting, errorsurface classifies every failed step cause as page, network, policy or gate with a retry hint carrying the policy verdict, and cancelrun rolls the queued steps back only while the executed steps stay untouched in the sealed log.
|
|
475
|
-
* No window, retention, limit or rollback scope is ever hardcoded: every length and expiry stays the user's choice with no engine cap, and no interface surface ever bypasses the human review.
|
|
476
|
-
*/
|
|
477
|
-
|
|
478
|
-
/** Stable local fingerprint of one indexed text so semanticrecall deduplicates repeated extractions; the hash stays deterministic without a network dependency. */
|
|
479
|
-
function textfingerprint(text: string): string {
|
|
480
|
-
let hash = 0x811c9dc5;
|
|
481
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
482
|
-
hash ^= text.charCodeAt(index);
|
|
483
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
484
|
-
}
|
|
485
|
-
return hash.toString(16).padStart(8, "0");
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Derives one keystream byte of the local at rest sealing from the note id and the byte position; the sealing stays a local measure and never replaces the user keychain. */
|
|
489
|
-
function keystreambyte(id: string, position: number): number {
|
|
490
|
-
let hash = 0x811c9dc5;
|
|
491
|
-
const source = `${id}:${position}`;
|
|
492
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
493
|
-
hash ^= source.charCodeAt(index);
|
|
494
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
495
|
-
}
|
|
496
|
-
return hash & 0xff;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/** Seals one sensitive note body at rest with a local keystream; the sealed text persists while the plain body never does. */
|
|
500
|
-
export function sealnotebody(id: string, body: string): string {
|
|
501
|
-
const sealed = Array.from(body, (character, index) =>
|
|
502
|
-
String.fromCharCode(character.charCodeAt(0) ^ keystreambyte(id, index)),
|
|
503
|
-
).join("");
|
|
504
|
-
return `sealed:${btoa(sealed)}`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Opens one sealed note body for the reading surface; the plain body leaves storage only through this reader. */
|
|
508
|
-
export function opennotebody(id: string, sealedbody: string): string {
|
|
509
|
-
if (!sealedbody.startsWith("sealed:")) return "";
|
|
510
|
-
try {
|
|
511
|
-
const sealed = atob(sealedbody.slice("sealed:".length));
|
|
512
|
-
return Array.from(sealed, (character, index) =>
|
|
513
|
-
String.fromCharCode(character.charCodeAt(0) ^ keystreambyte(id, index)),
|
|
514
|
-
).join("");
|
|
515
|
-
} catch {
|
|
516
|
-
return "";
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** Builds one site note record per origin: the title, the body, the author provenance and the timestamps; a sensitive note seals its body at rest while a plain note keeps its body readable. */
|
|
521
|
-
export function sitenoteof(input: {
|
|
522
|
-
origin: string;
|
|
523
|
-
title: string;
|
|
524
|
-
body: string;
|
|
525
|
-
author: string;
|
|
526
|
-
sensitive?: boolean;
|
|
527
|
-
now: number;
|
|
528
|
-
id?: string;
|
|
529
|
-
}): sitenote {
|
|
530
|
-
if (input.origin.trim() === "") throw new Error("The site note needs its origin.");
|
|
531
|
-
if (input.title.trim() === "") throw new Error("The site note needs its title.");
|
|
532
|
-
if (input.body.trim() === "") throw new Error("The site note needs its body.");
|
|
533
|
-
const id = input.id ?? randomid();
|
|
534
|
-
if (input.sensitive === true)
|
|
535
|
-
return {
|
|
536
|
-
id,
|
|
537
|
-
origin: input.origin,
|
|
538
|
-
title: input.title.trim(),
|
|
539
|
-
sealedbody: sealnotebody(id, input.body),
|
|
540
|
-
author: input.author,
|
|
541
|
-
sensitive: true,
|
|
542
|
-
createdat: input.now,
|
|
543
|
-
updatedat: input.now,
|
|
544
|
-
};
|
|
545
|
-
return {
|
|
546
|
-
id,
|
|
547
|
-
origin: input.origin,
|
|
548
|
-
title: input.title.trim(),
|
|
549
|
-
body: input.body,
|
|
550
|
-
author: input.author,
|
|
551
|
-
sensitive: false,
|
|
552
|
-
createdat: input.now,
|
|
553
|
-
updatedat: input.now,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** Reads the display body of one site note: a sensitive note opens its sealed body for the reader while a plain note returns its body. */
|
|
558
|
-
export function notebodyof(note: sitenote): string {
|
|
559
|
-
if (note.sensitive) return note.sealedbody !== undefined ? opennotebody(note.id, note.sealedbody) : "";
|
|
560
|
-
return note.body ?? "";
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/** Applies one edit to a site note: the edit keeps the author provenance of the writer and the updated timestamp while the created timestamp stays. */
|
|
564
|
-
export function editnote(
|
|
565
|
-
note: sitenote,
|
|
566
|
-
input: { title: string; body: string; author: string; now: number },
|
|
567
|
-
): sitenote {
|
|
568
|
-
if (input.title.trim() === "") throw new Error("The site note keeps a non empty title.");
|
|
569
|
-
if (input.body.trim() === "") throw new Error("The site note keeps a non empty body.");
|
|
570
|
-
if (note.sensitive)
|
|
571
|
-
return {
|
|
572
|
-
...note,
|
|
573
|
-
title: input.title.trim(),
|
|
574
|
-
sealedbody: sealnotebody(note.id, input.body),
|
|
575
|
-
updatedat: input.now,
|
|
576
|
-
author: input.author,
|
|
577
|
-
};
|
|
578
|
-
return { ...note, title: input.title.trim(), body: input.body, updatedat: input.now, author: input.author };
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/** Expires the site notes past the user configured window: an absent window keeps every note while the expired notes leave the store at the user boundary only. */
|
|
582
|
-
export function expirnotes(notes: sitenote[], retention: number | undefined, now: number): sitenote[] {
|
|
583
|
-
if (retention === undefined) return notes;
|
|
584
|
-
return notes.filter((note) => now - note.updatedat < retention);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/** Builds one append only scratchpad entry of one task with its step provenance; the scratchpad never rewrites a written entry. */
|
|
588
|
-
export function scratchentryof(input: {
|
|
589
|
-
taskid: string;
|
|
590
|
-
sessionid: string;
|
|
591
|
-
text: string;
|
|
592
|
-
stepid?: string;
|
|
593
|
-
author: string;
|
|
594
|
-
now: number;
|
|
595
|
-
id?: string;
|
|
596
|
-
}): scratchpadentry {
|
|
597
|
-
if (input.taskid.trim() === "") throw new Error("The scratchpad entry needs its task.");
|
|
598
|
-
if (input.text.trim() === "") throw new Error("The scratchpad entry needs its text.");
|
|
599
|
-
return {
|
|
600
|
-
id: input.id ?? randomid(),
|
|
601
|
-
taskid: input.taskid,
|
|
602
|
-
sessionid: input.sessionid,
|
|
603
|
-
text: input.text,
|
|
604
|
-
...(input.stepid !== undefined && input.stepid.trim() !== "" ? { stepid: input.stepid } : {}),
|
|
605
|
-
author: input.author,
|
|
606
|
-
at: input.now,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** Reads the scratchpad of one task only: entries of another task never cross the owning task session boundary. */
|
|
611
|
-
export function scratchpadof(entries: scratchpadentry[], taskid: string, sessionid: string): scratchpadentry[] {
|
|
612
|
-
return entries.filter((entry) => entry.taskid === taskid && entry.sessionid === sessionid);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/** Prunes the scratchpad entries past the user configured window: an absent window keeps every entry while the pruned entries leave the pad only at the user boundary. */
|
|
616
|
-
export function prunescratchpad(
|
|
617
|
-
entries: scratchpadentry[],
|
|
618
|
-
window: number | undefined,
|
|
619
|
-
now: number,
|
|
620
|
-
): scratchpadentry[] {
|
|
621
|
-
if (window === undefined) return entries;
|
|
622
|
-
return entries.filter((entry) => now - entry.at < window);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/** Distills one run summary from the completed run: the origins visited, the kinds executed and the per step outcomes inside the user configured window with no fixed cap; the distillation names its provenance. */
|
|
626
|
-
export function distillrunsummary(input: {
|
|
627
|
-
plan: agentplan;
|
|
628
|
-
outcomes: stepoutcome[];
|
|
629
|
-
origins: string[];
|
|
630
|
-
sessionid: string;
|
|
631
|
-
window?: number;
|
|
632
|
-
provenance: "offscreenworker" | "inline";
|
|
633
|
-
now: number;
|
|
634
|
-
}): runsummary {
|
|
635
|
-
const steps = input.outcomes.map((outcome) => {
|
|
636
|
-
const step = input.plan.steps.find((candidate) => candidate.id === outcome.stepid);
|
|
637
|
-
return { stepid: outcome.stepid, kind: step?.kind ?? "unknown", ok: outcome.ok, summary: outcome.summary };
|
|
638
|
-
});
|
|
639
|
-
const windowed =
|
|
640
|
-
input.window !== undefined && Number.isInteger(input.window) && input.window >= 0
|
|
641
|
-
? steps.slice(Math.max(0, steps.length - input.window))
|
|
642
|
-
: steps;
|
|
643
|
-
const kinds = [...new Set(windowed.map((step) => step.kind))];
|
|
644
|
-
return {
|
|
645
|
-
runid: input.plan.id,
|
|
646
|
-
sessionid: input.sessionid,
|
|
647
|
-
origins: [...new Set(input.origins)],
|
|
648
|
-
kinds,
|
|
649
|
-
steps: windowed,
|
|
650
|
-
...(input.window !== undefined && Number.isInteger(input.window) && input.window >= 0
|
|
651
|
-
? { window: input.window }
|
|
652
|
-
: {}),
|
|
653
|
-
task: "runsummary",
|
|
654
|
-
provenance: input.provenance,
|
|
655
|
-
distilledat: input.now,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/** Builds the runsummary search corpus entry of one distilled summary so historysearch indexes every summary beside the notes and the session metadata. */
|
|
660
|
-
export function summaryhistoryentry(summary: runsummary): historyindexentry {
|
|
661
|
-
return {
|
|
662
|
-
source: "summary",
|
|
663
|
-
id: summary.runid,
|
|
664
|
-
title: `Run summary of ${summary.runid}`,
|
|
665
|
-
text: `${summary.origins.join(" ")} ${summary.kinds.join(" ")} ${summary.steps.map((step) => step.summary).join(" ")}`,
|
|
666
|
-
outcome: summary.steps.every((step) => step.ok) ? "completed" : "failed",
|
|
667
|
-
at: summary.distilledat,
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/** Builds the sitenote search corpus entry of one note; a sensitive note indexes its title and origin only so the sealed body never enters the corpus. */
|
|
672
|
-
export function notehistoryentry(note: sitenote): historyindexentry {
|
|
673
|
-
return {
|
|
674
|
-
source: "note",
|
|
675
|
-
id: note.id,
|
|
676
|
-
...(note.origin !== "" ? { origin: note.origin } : {}),
|
|
677
|
-
title: note.title,
|
|
678
|
-
text: note.sensitive ? note.title : `${note.title} ${note.body ?? ""}`,
|
|
679
|
-
at: note.updatedat,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/** Builds one recall index entry from a past extraction record: the fingerprint deduplicates repeated extractions while the run and step provenance stays readable. */
|
|
684
|
-
export function recallentryof(input: {
|
|
685
|
-
origin: string;
|
|
686
|
-
runid: string;
|
|
687
|
-
stepid: string;
|
|
688
|
-
text: string;
|
|
689
|
-
at: number;
|
|
690
|
-
}): recallindexentry {
|
|
691
|
-
if (input.text.trim() === "") throw new Error("The recall index entry needs its text.");
|
|
692
|
-
if (input.stepid.trim() === "" || input.runid.trim() === "")
|
|
693
|
-
throw new Error("The recall index entry needs its run and step provenance.");
|
|
694
|
-
const normalized = input.text.trim().replace(/\s+/g, " ");
|
|
695
|
-
return {
|
|
696
|
-
fingerprint: textfingerprint(normalized),
|
|
697
|
-
origin: input.origin,
|
|
698
|
-
runid: input.runid,
|
|
699
|
-
stepid: input.stepid,
|
|
700
|
-
text: normalized,
|
|
701
|
-
at: input.at,
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/** Adds one recall index entry with fingerprint deduplication: a repeated extraction keeps its first index entry while later duplicates never double rank. */
|
|
706
|
-
export function addrecallentry(index: recallindexentry[], entry: recallindexentry): recallindexentry[] {
|
|
707
|
-
if (index.some((candidate) => candidate.fingerprint === entry.fingerprint && candidate.origin === entry.origin))
|
|
708
|
-
return index;
|
|
709
|
-
return [...index, entry];
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/** Splits one text into its lowercase term set for the local text similarity ranking. */
|
|
713
|
-
function termsof(text: string): Set<string> {
|
|
714
|
-
return new Set(
|
|
715
|
-
text
|
|
716
|
-
.toLowerCase()
|
|
717
|
-
.split(/[^a-z0-9]+/)
|
|
718
|
-
.filter((term) => term.length > 1),
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/** Ranks the indexed extractions by local text similarity inside the worker: the jaccard overlap of the query terms and the entry terms, scoped to the origins of the run scope, with the provenance of every match. */
|
|
723
|
-
export function rankrecall(index: recallindexentry[], query: recallquery, scope: { origins: string[] }): recallmatch[] {
|
|
724
|
-
if (query.text.trim() === "") return [];
|
|
725
|
-
const terms = termsof(query.text);
|
|
726
|
-
const scoped = query.origin !== undefined && query.origin.trim() !== "" ? [query.origin] : scope.origins;
|
|
727
|
-
const matches: recallmatch[] = [];
|
|
728
|
-
for (const entry of index) {
|
|
729
|
-
if (!scoped.includes(entry.origin)) continue;
|
|
730
|
-
const entryterms = termsof(entry.text);
|
|
731
|
-
let shared = 0;
|
|
732
|
-
for (const term of terms) if (entryterms.has(term)) shared += 1;
|
|
733
|
-
const union = new Set([...terms, ...entryterms]).size;
|
|
734
|
-
const score = union === 0 ? 0 : shared / union;
|
|
735
|
-
if (score <= 0) continue;
|
|
736
|
-
matches.push({
|
|
737
|
-
entry,
|
|
738
|
-
score,
|
|
739
|
-
reason: `The extraction of ${entry.origin} shares ${shared} term${shared === 1 ? "" : "s"} with the query at the score ${score.toFixed(3)}; the match carries the run ${entry.runid} and the step ${entry.stepid}.`,
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
const ranked = matches.sort((one, two) => two.score - one.score);
|
|
743
|
-
return query.limit !== undefined && Number.isInteger(query.limit) && query.limit >= 0
|
|
744
|
-
? ranked.slice(0, query.limit)
|
|
745
|
-
: ranked;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/** Expires the recall index entries past the user configured window: the live index keeps the window while the extraction records themselves stay for the audit trail. */
|
|
749
|
-
export function expirerecallindex(
|
|
750
|
-
index: recallindexentry[],
|
|
751
|
-
window: number | undefined,
|
|
752
|
-
now: number,
|
|
753
|
-
): recallindexentry[] {
|
|
754
|
-
if (window === undefined) return index;
|
|
755
|
-
return index.filter((entry) => now - entry.at < window);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/** Builds one correction memory entry from a plan review edit: the step shape before and after the edit with the reason in plain language. */
|
|
759
|
-
export function editedcorrectionof(input: {
|
|
760
|
-
origin: string;
|
|
761
|
-
kind: string;
|
|
762
|
-
stepid: string;
|
|
763
|
-
original: string;
|
|
764
|
-
corrected: string;
|
|
765
|
-
reason: string;
|
|
766
|
-
now: number;
|
|
767
|
-
id?: string;
|
|
768
|
-
}): correctionentry {
|
|
769
|
-
if (input.stepid.trim() === "" || input.kind.trim() === "")
|
|
770
|
-
throw new Error("The correction needs its step and kind.");
|
|
771
|
-
if (input.original === input.corrected) throw new Error("The correction needs a changed step shape.");
|
|
772
|
-
return {
|
|
773
|
-
id: input.id ?? randomid(),
|
|
774
|
-
origin: input.origin,
|
|
775
|
-
kind: input.kind,
|
|
776
|
-
stepid: input.stepid,
|
|
777
|
-
source: "edited",
|
|
778
|
-
original: input.original,
|
|
779
|
-
corrected: input.corrected,
|
|
780
|
-
reason: input.reason,
|
|
781
|
-
at: input.now,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/** Builds one correction memory entry from a rejected step: the step shape it refused with the rejection reason. */
|
|
786
|
-
export function rejectedcorrectionof(input: {
|
|
787
|
-
origin: string;
|
|
788
|
-
kind: string;
|
|
789
|
-
stepid: string;
|
|
790
|
-
original: string;
|
|
791
|
-
reason: string;
|
|
792
|
-
now: number;
|
|
793
|
-
id?: string;
|
|
794
|
-
}): correctionentry {
|
|
795
|
-
if (input.stepid.trim() === "" || input.reason.trim() === "")
|
|
796
|
-
throw new Error("The rejected correction needs its step and its rejection reason.");
|
|
797
|
-
return {
|
|
798
|
-
id: input.id ?? randomid(),
|
|
799
|
-
origin: input.origin,
|
|
800
|
-
kind: input.kind,
|
|
801
|
-
stepid: input.stepid,
|
|
802
|
-
source: "rejected",
|
|
803
|
-
original: input.original,
|
|
804
|
-
reason: input.reason,
|
|
805
|
-
at: input.now,
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/** Reads the past corrections that match a proposed step by origin and kind so proposal requests feed the matching history in. */
|
|
810
|
-
export function matchingcorrections(
|
|
811
|
-
corrections: correctionentry[],
|
|
812
|
-
proposal: { origin: string; kind: string },
|
|
813
|
-
): correctionentry[] {
|
|
814
|
-
return corrections.filter((entry) => entry.origin === proposal.origin && entry.kind === proposal.kind);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/** Expires the correction memory entries past the user configured window: an absent window keeps every correction. */
|
|
818
|
-
export function expirecorrections(
|
|
819
|
-
corrections: correctionentry[],
|
|
820
|
-
window: number | undefined,
|
|
821
|
-
now: number,
|
|
822
|
-
): correctionentry[] {
|
|
823
|
-
if (window === undefined) return corrections;
|
|
824
|
-
return corrections.filter((entry) => now - entry.at < window);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/** Builds one consent memory entry per origin: every grant, denial, expiry and revocation lands with its boundary and kinds while the record stays advisory. */
|
|
828
|
-
export function consentmemoryof(input: {
|
|
829
|
-
origin: string;
|
|
830
|
-
decision: "grant" | "deny" | "expire" | "revoke";
|
|
831
|
-
boundary: string;
|
|
832
|
-
kinds: string[];
|
|
833
|
-
expiresat?: number;
|
|
834
|
-
now: number;
|
|
835
|
-
id?: string;
|
|
836
|
-
}): consentmemoryentry {
|
|
837
|
-
if (input.origin.trim() === "") throw new Error("The consent memory entry needs its origin.");
|
|
838
|
-
if (input.boundary.trim() === "") throw new Error("The consent memory entry needs the boundary the prompt named.");
|
|
839
|
-
return {
|
|
840
|
-
id: input.id ?? randomid(),
|
|
841
|
-
origin: input.origin,
|
|
842
|
-
decision: input.decision,
|
|
843
|
-
boundary: input.boundary,
|
|
844
|
-
kinds: [...new Set(input.kinds)],
|
|
845
|
-
at: input.now,
|
|
846
|
-
...(input.expiresat !== undefined ? { expiresat: input.expiresat } : {}),
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/** Reads the prior consent decisions of one origin for the prompt surface: the history stays advisory, the denial records carry the same weight as the grants and no entry ever auto grants. */
|
|
851
|
-
export function consentadvisory(entries: consentmemoryentry[], origin: string, now: number): consentmemoryentry[] {
|
|
852
|
-
return entries.filter((entry) => entry.origin === origin && (entry.expiresat === undefined || entry.expiresat > now));
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/** Reads the advisory verdict of the consent memory for a new prompt: the latest decision names itself, a refusal keeps its refusal and no history widens a boundary. */
|
|
856
|
-
export function consentadvisoryverdict(
|
|
857
|
-
entries: consentmemoryentry[],
|
|
858
|
-
origin: string,
|
|
859
|
-
kind: string,
|
|
860
|
-
): { advisory: boolean; reason: string } {
|
|
861
|
-
const matching = entries.filter((entry) => entry.origin === origin && entry.kinds.includes(kind));
|
|
862
|
-
const latest = matching[matching.length - 1];
|
|
863
|
-
if (latest === undefined)
|
|
864
|
-
return {
|
|
865
|
-
advisory: false,
|
|
866
|
-
reason: `No prior decision exists for the ${kind} kind on ${origin}; the prompt opens fresh.`,
|
|
867
|
-
};
|
|
868
|
-
if (latest.decision === "deny")
|
|
869
|
-
return {
|
|
870
|
-
advisory: true,
|
|
871
|
-
reason: `The consent memory holds a prior denial of the ${kind} kind on ${origin} from ${new Date(latest.at).toISOString()}; the denial carries the same weight as a grant and the record stays advisory only.`,
|
|
872
|
-
};
|
|
873
|
-
return {
|
|
874
|
-
advisory: true,
|
|
875
|
-
reason: `The consent memory holds a prior ${latest.decision} of the ${kind} kind on ${origin} with the boundary ${latest.boundary}; the record advises the new prompt and never auto grants.`,
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/** Computes the queued and the executed steps of one run from the plan and its progress: the queued steps never executed while the executed steps stay in the sealed log. */
|
|
880
|
-
export function rollbacksplit(
|
|
881
|
-
plan: agentplan | undefined,
|
|
882
|
-
progress: planprogress | undefined,
|
|
883
|
-
): { executedstepids: string[]; queuedstepids: string[] } {
|
|
884
|
-
const executed = progress && progress.planid === plan?.id ? progress.completedsteps : [];
|
|
885
|
-
const executedset = new Set(executed);
|
|
886
|
-
const queued = (plan?.steps ?? []).map((step) => step.id).filter((id) => !executedset.has(id));
|
|
887
|
-
return { executedstepids: executed, queuedstepids: queued };
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/** Builds the rollback option descriptor of one cancelrun: the queued scope rolls the queued steps back only while the executed steps stay untouched in the immutable log. */
|
|
891
|
-
export function rollbackof(
|
|
892
|
-
plan: agentplan | undefined,
|
|
893
|
-
progress: planprogress | undefined,
|
|
894
|
-
preference: "queued" | "none" | undefined,
|
|
895
|
-
): rollbackdescriptor {
|
|
896
|
-
const split = rollbacksplit(plan, progress);
|
|
897
|
-
if (preference === "none")
|
|
898
|
-
return {
|
|
899
|
-
scope: "none",
|
|
900
|
-
label: `Stop the run ${plan?.id ?? ""} without a rollback; the ${split.queuedstepids.length} queued step${split.queuedstepids.length === 1 ? "" : "s"} stay as the run left them.`,
|
|
901
|
-
queuedstepids: split.queuedstepids,
|
|
902
|
-
};
|
|
903
|
-
return {
|
|
904
|
-
scope: "queued",
|
|
905
|
-
label: `Cancel the run ${plan?.id ?? ""} and roll its ${split.queuedstepids.length} queued step${split.queuedstepids.length === 1 ? "" : "s"} back${split.queuedstepids.length > 0 ? ` (${split.queuedstepids.join(", ")})` : ""} while the ${split.executedstepids.length} executed step${split.executedstepids.length === 1 ? "" : "s"} stay untouched in the sealed log.`,
|
|
906
|
-
queuedstepids: split.queuedstepids,
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/** Builds one cancelrun action with its rollback option descriptor; the action never touches the sealed log of the executed steps. */
|
|
911
|
-
export function cancelrunactionof(input: {
|
|
912
|
-
runid: string;
|
|
913
|
-
sessionid: string;
|
|
914
|
-
plan: agentplan | undefined;
|
|
915
|
-
progress: planprogress | undefined;
|
|
916
|
-
preference: "queued" | "none" | undefined;
|
|
917
|
-
}): cancelrunaction {
|
|
918
|
-
return {
|
|
919
|
-
runid: input.runid,
|
|
920
|
-
sessionid: input.sessionid,
|
|
921
|
-
rollback: rollbackof(input.plan, input.progress, input.preference),
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/** Builds one error surface payload of a failed step: the cause class, the message, the retry hint with the policy verdict and the context facts. */
|
|
926
|
-
export function errorsurfaceof(input: {
|
|
927
|
-
stepid: string;
|
|
928
|
-
runid: string;
|
|
929
|
-
message: string;
|
|
930
|
-
cause: errorcause;
|
|
931
|
-
retryallowed: boolean;
|
|
932
|
-
retryreason: string;
|
|
933
|
-
context: Record<string, string>;
|
|
934
|
-
now: number;
|
|
935
|
-
}): errorsurface {
|
|
936
|
-
if (input.message.trim() === "") throw new Error("The error surface needs its message in plain language.");
|
|
937
|
-
return {
|
|
938
|
-
stepid: input.stepid,
|
|
939
|
-
runid: input.runid,
|
|
940
|
-
cause: input.cause,
|
|
941
|
-
message: input.message,
|
|
942
|
-
retry: { allowed: input.retryallowed, reason: input.retryreason },
|
|
943
|
-
context: input.context,
|
|
944
|
-
at: input.now,
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** Classifies the cause of one failed step: a policy refusal and a gate wait carry their own classes, a network shaped message names the network and every other failure stays with the page. */
|
|
949
|
-
export function classifyfailure(input: { message: string; policyrefused: boolean; gatewait: boolean }): errorcause {
|
|
950
|
-
if (input.gatewait) return "gate";
|
|
951
|
-
if (input.policyrefused) return "policy";
|
|
952
|
-
if (/\b(network|offline|timeout|timed out|fetch failed|socket|dns|connection)\b/i.test(input.message))
|
|
953
|
-
return "network";
|
|
954
|
-
return "page";
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/** Reads the retry hint of one error surface: a retry passes only through a new reviewed dispatch, so the verdict names the reviewed path instead of an automatic one. */
|
|
958
|
-
export function retryhintof(surface: errorsurface): { allowed: boolean; reason: string } {
|
|
959
|
-
if (!surface.retry.allowed)
|
|
960
|
-
return {
|
|
961
|
-
allowed: false,
|
|
962
|
-
reason: `The ${surface.cause} failure of the step ${surface.stepid} refuses the retry: ${surface.retry.reason}`,
|
|
963
|
-
};
|
|
964
|
-
return {
|
|
965
|
-
allowed: true,
|
|
966
|
-
reason: `The ${surface.cause} failure of the step ${surface.stepid} may retry through a new reviewed dispatch; the retry rides the full consent gate chain and never bypasses the review.`,
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
/** Derives the session grid rows from the existing session stores with no new state: the live run derives from the session, the plan and its progress, the saved runs derive from the sealed logs and their summaries, and the per tab lock state names the concurrent holder. */
|
|
971
|
-
export function sessiongridrows(input: {
|
|
972
|
-
session?: { id: string; tabid: number; origin: string; stoppedat?: number; pausedat?: number; grants?: string[] };
|
|
973
|
-
plan?: agentplan;
|
|
974
|
-
progress?: planprogress;
|
|
975
|
-
logs: storedrunlog[];
|
|
976
|
-
summaries: runsummary[];
|
|
977
|
-
locks: Array<{ runid: string }>;
|
|
978
|
-
tabsessions: tabsessionref[];
|
|
979
|
-
}): sessiongridrow[] {
|
|
980
|
-
const rows: sessiongridrow[] = [];
|
|
981
|
-
if (input.session && input.plan && ["pending", "approved"].includes(input.plan.state)) {
|
|
982
|
-
const split = rollbacksplit(input.plan, input.progress);
|
|
983
|
-
const held = input.locks.some((lock) => lock.runid === input.plan?.id);
|
|
984
|
-
const origins = [...new Set([input.session.origin, ...(input.session.grants ?? [])])];
|
|
985
|
-
const actions: Array<"resume" | "cancelrun" | "reopen"> = ["cancelrun"];
|
|
986
|
-
if (input.session.pausedat !== undefined) actions.push("resume");
|
|
987
|
-
rows.push({
|
|
988
|
-
sessionid: input.session.id,
|
|
989
|
-
runid: input.plan.id,
|
|
990
|
-
origins,
|
|
991
|
-
state: "live",
|
|
992
|
-
outcome: `${split.executedstepids.length} of ${input.plan.steps.length} reviewed steps executed`,
|
|
993
|
-
steps: input.plan.steps.length,
|
|
994
|
-
completed: split.executedstepids.length,
|
|
995
|
-
lock: held ? "held" : "free",
|
|
996
|
-
tabid: input.session.tabid,
|
|
997
|
-
updatedat: input.plan.createdat,
|
|
998
|
-
actions,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
for (const log of input.logs) {
|
|
1002
|
-
const summary = input.summaries.find((candidate) => candidate.runid === log.runid);
|
|
1003
|
-
const tabsession = input.tabsessions.find((candidate) => candidate.runid === log.runid);
|
|
1004
|
-
const held = input.locks.some((lock) => lock.runid === log.runid);
|
|
1005
|
-
rows.push({
|
|
1006
|
-
sessionid: log.sessionid,
|
|
1007
|
-
runid: log.runid,
|
|
1008
|
-
origins: [...new Set(log.entries.map((entry) => entry.origin).filter((origin) => origin !== ""))],
|
|
1009
|
-
state: "saved",
|
|
1010
|
-
outcome:
|
|
1011
|
-
summary !== undefined
|
|
1012
|
-
? summary.steps.every((step) => step.ok)
|
|
1013
|
-
? "completed"
|
|
1014
|
-
: "failed"
|
|
1015
|
-
: log.seal !== undefined
|
|
1016
|
-
? "sealed"
|
|
1017
|
-
: "open",
|
|
1018
|
-
steps: summary?.steps.length ?? log.entries.filter((entry) => entry.kind === "step").length,
|
|
1019
|
-
completed:
|
|
1020
|
-
summary?.steps.filter((step) => step.ok).length ?? log.entries.filter((entry) => entry.kind === "step").length,
|
|
1021
|
-
lock: held ? "held" : "free",
|
|
1022
|
-
...(log.seal !== undefined ? { sealhash: log.seal.sealhash.current } : {}),
|
|
1023
|
-
...(tabsession !== undefined ? { tabid: tabsession.tabid } : {}),
|
|
1024
|
-
updatedat: log.updatedat,
|
|
1025
|
-
actions: ["reopen"],
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
return rows.sort((one, two) => two.updatedat - one.updatedat);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/** Normalizes one history search query: the non empty text with the optional origin, time range and outcome filters. */
|
|
1032
|
-
export function historyqueryof(value: unknown): historysearchquery | undefined {
|
|
1033
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1034
|
-
const candidate = value as Record<string, unknown>;
|
|
1035
|
-
if (typeof candidate.text !== "string" || candidate.text.trim() === "") return undefined;
|
|
1036
|
-
const origin =
|
|
1037
|
-
typeof candidate.origin === "string" && candidate.origin.trim() !== "" ? candidate.origin.trim() : undefined;
|
|
1038
|
-
const from = typeof candidate.from === "number" && Number.isFinite(candidate.from) ? candidate.from : undefined;
|
|
1039
|
-
const to = typeof candidate.to === "number" && Number.isFinite(candidate.to) ? candidate.to : undefined;
|
|
1040
|
-
if (from !== undefined && to !== undefined && from > to) return undefined;
|
|
1041
|
-
const outcome =
|
|
1042
|
-
typeof candidate.outcome === "string" && candidate.outcome.trim() !== "" ? candidate.outcome.trim() : undefined;
|
|
1043
|
-
return {
|
|
1044
|
-
text: candidate.text.trim(),
|
|
1045
|
-
...(origin !== undefined ? { origin } : {}),
|
|
1046
|
-
...(from !== undefined ? { from } : {}),
|
|
1047
|
-
...(to !== undefined ? { to } : {}),
|
|
1048
|
-
...(outcome !== undefined ? { outcome } : {}),
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
/** Adds one corpus entry to the incremental history index: each store write lands in the index at once so the search reads a growing corpus with no rebuild. */
|
|
1053
|
-
export function addhistoryentry(corpus: historyindexentry[], entry: historyindexentry): historyindexentry[] {
|
|
1054
|
-
return [entry, ...corpus.filter((candidate) => !(candidate.source === entry.source && candidate.id === entry.id))];
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/** Extracts the matched terms of one query inside one text, case insensitive, for the result highlighting. */
|
|
1058
|
-
export function highlightterms(text: string, query: string): string[] {
|
|
1059
|
-
const terms = query
|
|
1060
|
-
.toLowerCase()
|
|
1061
|
-
.split(/[^a-z0-9]+/)
|
|
1062
|
-
.filter((term) => term.length > 1);
|
|
1063
|
-
const lower = text.toLowerCase();
|
|
1064
|
-
return [...new Set(terms.filter((term) => lower.includes(term)))];
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/** Searches the one corpus of session metadata, notes and run summaries: the text matches with the matched terms highlighted, filtered by origin, time range and outcome. */
|
|
1068
|
-
export function historysearch(corpus: historyindexentry[], query: historysearchquery): historysearchhit[] {
|
|
1069
|
-
const terms = query.text
|
|
1070
|
-
.toLowerCase()
|
|
1071
|
-
.split(/[^a-z0-9]+/)
|
|
1072
|
-
.filter((term) => term.length > 1);
|
|
1073
|
-
const hits: historysearchhit[] = [];
|
|
1074
|
-
for (const entry of corpus) {
|
|
1075
|
-
if (query.origin !== undefined && entry.origin !== query.origin) continue;
|
|
1076
|
-
if (query.from !== undefined && entry.at < query.from) continue;
|
|
1077
|
-
if (query.to !== undefined && entry.at > query.to) continue;
|
|
1078
|
-
if (query.outcome !== undefined && entry.outcome !== query.outcome) continue;
|
|
1079
|
-
const haystack = `${entry.title} ${entry.text}`.toLowerCase();
|
|
1080
|
-
const matched = terms.filter((term) => haystack.includes(term));
|
|
1081
|
-
if (matched.length === 0) continue;
|
|
1082
|
-
const position = haystack.indexOf(matched[0] ?? "");
|
|
1083
|
-
const start = Math.max(0, position - 40);
|
|
1084
|
-
const excerpt = `${start > 0 ? "…" : ""}${`${entry.title} ${entry.text}`.slice(start, start + 160)}${start + 160 < `${entry.title} ${entry.text}`.length ? "…" : ""}`;
|
|
1085
|
-
hits.push({
|
|
1086
|
-
source: entry.source,
|
|
1087
|
-
id: entry.id,
|
|
1088
|
-
title: entry.title,
|
|
1089
|
-
excerpt,
|
|
1090
|
-
highlights: [...new Set(matched)],
|
|
1091
|
-
...(entry.origin !== undefined ? { origin: entry.origin } : {}),
|
|
1092
|
-
...(entry.outcome !== undefined ? { outcome: entry.outcome } : {}),
|
|
1093
|
-
at: entry.at,
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
return hits.sort((one, two) => two.at - one.at);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/** Serves the empty state guidance of one session interface surface in plain language: the grid explains the first run, the search box suggests a first query, the notes explain the note flow and the scratchpad explains how the agent writes. */
|
|
1100
|
-
export function emptystatemessage(surface: emptystatesurface, origin?: string): string {
|
|
1101
|
-
if (surface === "historysearch")
|
|
1102
|
-
return "No history matches yet; start with a first query such as an origin, a note title or a kind the runs executed.";
|
|
1103
|
-
if (surface === "sitenotes")
|
|
1104
|
-
return `No site note exists${origin !== undefined ? ` for ${origin}` : ""} yet; write the first note with a title and a body and the note flow keeps it per origin with its author provenance.`;
|
|
1105
|
-
if (surface === "scratchpad")
|
|
1106
|
-
return "The scratchpad holds no entry yet; the agent appends its per task notes here while the reviewed steps run, and every entry carries its step provenance.";
|
|
1107
|
-
return "No session exists yet; start the first run by describing an objective and reviewing the plan the agent proposes.";
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/** Builds the per tab isolation key of one tab so parallel tabs never collide inside the session stores. */
|
|
1111
|
-
export function tabsessionkey(tabid: number): string {
|
|
1112
|
-
return `tabsession:${tabid}`;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
/** Builds one per tab session reference that isolates the session state per tab: parallel tabs keep their own session, run and origin references. */
|
|
1116
|
-
export function tabsessionrefof(input: {
|
|
1117
|
-
tabid: number;
|
|
1118
|
-
sessionid: string;
|
|
1119
|
-
runid?: string;
|
|
1120
|
-
origin: string;
|
|
1121
|
-
now: number;
|
|
1122
|
-
}): tabsessionref {
|
|
1123
|
-
if (!Number.isInteger(input.tabid) || input.tabid < 0)
|
|
1124
|
-
throw new Error("The per tab session reference needs its tab.");
|
|
1125
|
-
if (input.sessionid.trim() === "") throw new Error("The per tab session reference needs its session.");
|
|
1126
|
-
return {
|
|
1127
|
-
tabid: input.tabid,
|
|
1128
|
-
sessionid: input.sessionid,
|
|
1129
|
-
...(input.runid !== undefined && input.runid.trim() !== "" ? { runid: input.runid } : {}),
|
|
1130
|
-
origin: input.origin,
|
|
1131
|
-
updatedat: input.now,
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
/** Exports the site notes, the run summaries and the correction memory as one audit bundle: every record carries its provenance while sensitive note bodies stay sealed. */
|
|
1136
|
-
export function sessionbundleof(input: {
|
|
1137
|
-
notes: sitenote[];
|
|
1138
|
-
summaries: runsummary[];
|
|
1139
|
-
corrections: correctionentry[];
|
|
1140
|
-
exportedat: number;
|
|
1141
|
-
}): {
|
|
1142
|
-
kind: "sessionbundle";
|
|
1143
|
-
notes: sitenote[];
|
|
1144
|
-
summaries: runsummary[];
|
|
1145
|
-
corrections: correctionentry[];
|
|
1146
|
-
exportedat: number;
|
|
1147
|
-
} {
|
|
1148
|
-
return {
|
|
1149
|
-
kind: "sessionbundle",
|
|
1150
|
-
notes: input.notes,
|
|
1151
|
-
summaries: input.summaries,
|
|
1152
|
-
corrections: input.corrections,
|
|
1153
|
-
exportedat: input.exportedat,
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
/**
|
|
1158
|
-
* The recall seam the memory adapter documents for the future remote recall backend: today the local fingerprint index answers every semanticrecall query inside the extension, and the seam keeps the query and answer shapes stable so a reviewed remote backend can take the calls later without touching the callers.
|
|
1159
|
-
*/
|
|
1160
|
-
export interface recallseam {
|
|
1161
|
-
recall(query: recallquery, scope: { origins: string[] }): Promise<recallmatch[]>;
|
|
1162
|
-
}
|