datanotes 2.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +9 -0
- package/README.md +203 -28
- package/SECURITY.md +92 -45
- package/dist/THIRD-PARTY-NOTICES.txt +682 -0
- package/dist/api/catalog.d.cts +69 -0
- package/dist/api/catalog.d.ts +50 -2
- package/dist/api/cli-options.d.cts +39 -0
- package/dist/api/cli-options.d.ts +39 -0
- package/dist/api/client.d.cts +122 -0
- package/dist/api/client.d.ts +56 -34
- package/dist/api/docs.d.cts +211 -0
- package/dist/api/docs.d.ts +211 -0
- package/dist/api/guidance.d.cts +81 -0
- package/dist/api/guidance.d.ts +81 -0
- package/dist/api/index.cjs +7470 -2771
- package/dist/api/index.d.cts +14 -0
- package/dist/api/index.d.ts +11 -7
- package/dist/api/index.js +7470 -2771
- package/dist/api/local.cjs +118 -6
- package/dist/api/local.d.cts +95 -0
- package/dist/api/local.d.ts +70 -6
- package/dist/api/local.js +104 -6
- package/dist/api/mcp.cjs +17387 -0
- package/dist/api/mcp.d.cts +108 -0
- package/dist/api/mcp.d.ts +108 -0
- package/dist/api/mcp.js +17337 -0
- package/dist/api/otlp.d.cts +113 -0
- package/dist/api/otlp.d.ts +63 -15
- package/dist/api/redact.d.cts +42 -0
- package/dist/api/redact.d.ts +42 -0
- package/dist/api/requests.d.cts +26 -0
- package/dist/api/requests.d.ts +14 -16
- package/dist/api/results.d.cts +16 -0
- package/dist/api/results.d.ts +2 -1
- package/dist/api/tokens.d.cts +12 -0
- package/dist/api/tokens.d.ts +5 -0
- package/dist/api/tracing.d.cts +107 -0
- package/dist/api/tracing.d.ts +107 -0
- package/dist/datanotes.cjs +69597 -28329
- package/dist/docs/administration/configuration.md +59 -0
- package/dist/docs/administration/docker.md +80 -0
- package/dist/docs/administration/git-sync.md +71 -0
- package/dist/docs/administration/limits.md +153 -0
- package/dist/docs/administration/run-the-service.md +237 -0
- package/dist/docs/administration/status.md +61 -0
- package/dist/docs/administration/use-with-obsidian.md +75 -0
- package/dist/docs/api/operations/add_column.md +49 -0
- package/dist/docs/api/operations/add_constraint.md +55 -0
- package/dist/docs/api/operations/adopt_rows.md +50 -0
- package/dist/docs/api/operations/align_relations.md +41 -0
- package/dist/docs/api/operations/alter_column.md +46 -0
- package/dist/docs/api/operations/alter_table.md +67 -0
- package/dist/docs/api/operations/batch.md +48 -0
- package/dist/docs/api/operations/changes_stream.md +28 -0
- package/dist/docs/api/operations/check_table.md +29 -0
- package/dist/docs/api/operations/clone_table.md +48 -0
- package/dist/docs/api/operations/comment_on.md +42 -0
- package/dist/docs/api/operations/configure_database.md +52 -0
- package/dist/docs/api/operations/create_table.md +78 -0
- package/dist/docs/api/operations/create_view.md +54 -0
- package/dist/docs/api/operations/create_webhook.md +37 -0
- package/dist/docs/api/operations/define_base.md +52 -0
- package/dist/docs/api/operations/delete_rows.md +43 -0
- package/dist/docs/api/operations/delete_webhook.md +31 -0
- package/dist/docs/api/operations/describe_database.md +30 -0
- package/dist/docs/api/operations/describe_history.md +40 -0
- package/dist/docs/api/operations/describe_semantic_index.md +24 -0
- package/dist/docs/api/operations/describe_table.md +38 -0
- package/dist/docs/api/operations/docs.md +13 -0
- package/dist/docs/api/operations/drop_column.md +41 -0
- package/dist/docs/api/operations/drop_constraint.md +39 -0
- package/dist/docs/api/operations/drop_table.md +43 -0
- package/dist/docs/api/operations/drop_view.md +38 -0
- package/dist/docs/api/operations/engine_status.md +23 -0
- package/dist/docs/api/operations/graph_neighbors.md +34 -0
- package/dist/docs/api/operations/graph_path.md +32 -0
- package/dist/docs/api/operations/graph_traverse.md +43 -0
- package/dist/docs/api/operations/identify_session.md +24 -0
- package/dist/docs/api/operations/infer_table.md +43 -0
- package/dist/docs/api/operations/insert_row.md +48 -0
- package/dist/docs/api/operations/list_changes.md +35 -0
- package/dist/docs/api/operations/list_folder.md +28 -0
- package/dist/docs/api/operations/list_link_targets.md +44 -0
- package/dist/docs/api/operations/list_migrations.md +35 -0
- package/dist/docs/api/operations/list_tables.md +32 -0
- package/dist/docs/api/operations/list_webhooks.md +23 -0
- package/dist/docs/api/operations/manual_index.md +14 -0
- package/dist/docs/api/operations/manual_page.md +22 -0
- package/dist/docs/api/operations/merge_rows.md +44 -0
- package/dist/docs/api/operations/move_rows.md +52 -0
- package/dist/docs/api/operations/openapi.md +13 -0
- package/dist/docs/api/operations/openapi_post.md +20 -0
- package/dist/docs/api/operations/read_note.md +29 -0
- package/dist/docs/api/operations/recompute_columns.md +44 -0
- package/dist/docs/api/operations/record_tool_call.md +37 -0
- package/dist/docs/api/operations/rename_column.md +44 -0
- package/dist/docs/api/operations/rename_value.md +45 -0
- package/dist/docs/api/operations/restore_database.md +51 -0
- package/dist/docs/api/operations/restore_table.md +46 -0
- package/dist/docs/api/operations/search_notes.md +28 -0
- package/dist/docs/api/operations/select_rows.md +61 -0
- package/dist/docs/api/operations/semantic_search.md +34 -0
- package/dist/docs/api/operations/semantic_search_docs.md +26 -0
- package/dist/docs/api/operations/set_table_bases.md +44 -0
- package/dist/docs/api/operations/status.md +13 -0
- package/dist/docs/api/operations/stop_engine.md +23 -0
- package/dist/docs/api/operations/sync_semantic_index.md +33 -0
- package/dist/docs/api/operations/table_changes.md +31 -0
- package/dist/docs/api/operations/tags/auxiliary.md +14 -0
- package/dist/docs/api/operations/tags/change-feed.md +8 -0
- package/dist/docs/api/operations/tags/history.md +17 -0
- package/dist/docs/api/operations/tags/queries.md +15 -0
- package/dist/docs/api/operations/tags/rows-dml.md +16 -0
- package/dist/docs/api/operations/tags/schema-ddl.md +22 -0
- package/dist/docs/api/operations/tags/service.md +15 -0
- package/dist/docs/api/operations/tags/tokens.md +13 -0
- package/dist/docs/api/operations/tokens_create.md +34 -0
- package/dist/docs/api/operations/tokens_delete.md +29 -0
- package/dist/docs/api/operations/tokens_list.md +21 -0
- package/dist/docs/api/operations/tokens_me.md +20 -0
- package/dist/docs/api/operations/tokens_rotate.md +29 -0
- package/dist/docs/api/operations/tokens_update.md +34 -0
- package/dist/docs/api/operations/undo_migration.md +46 -0
- package/dist/docs/api/operations/update_row.md +60 -0
- package/dist/docs/api/operations/update_rows.md +46 -0
- package/dist/docs/api/operations/vacuum_table.md +37 -0
- package/dist/docs/api.md +135 -0
- package/dist/docs/develop/build-an-app.md +320 -0
- package/dist/docs/develop/build-on-datanotes.md +138 -0
- package/dist/docs/develop/change-feed-and-webhooks.md +70 -0
- package/dist/docs/develop/client.md +116 -0
- package/dist/docs/develop/concurrency.md +188 -0
- package/dist/docs/develop/http-api.md +89 -0
- package/dist/docs/develop/telemetry.md +285 -0
- package/dist/docs/guides/agents/docs-offline.md +99 -0
- package/dist/docs/guides/agents/how-agents-use-datanotes.md +124 -0
- package/dist/docs/guides/agents/prompts.md +495 -0
- package/dist/docs/guides/agents/reading-errors.md +112 -0
- package/dist/docs/guides/data-quality/check-constraints.md +379 -0
- package/dist/docs/guides/data-quality/check-table.md +63 -0
- package/dist/docs/guides/data-quality/hand-edits.md +507 -0
- package/dist/docs/guides/data-quality/sync-between-devices.md +51 -0
- package/dist/docs/guides/data-quality/validation.md +90 -0
- package/dist/docs/guides/database-objects/bases.md +480 -0
- package/dist/docs/guides/database-objects/catalogs-and-schemas.md +897 -0
- package/dist/docs/guides/database-objects/columns.md +1332 -0
- package/dist/docs/guides/database-objects/relations.md +1454 -0
- package/dist/docs/guides/database-objects/tables.md +1340 -0
- package/dist/docs/guides/database-objects/views.md +1003 -0
- package/dist/docs/guides/history/back-up.md +203 -0
- package/dist/docs/guides/history/change-data-feed.md +247 -0
- package/dist/docs/guides/history/journal-and-undo.md +262 -0
- package/dist/docs/guides/history/restore.md +341 -0
- package/dist/docs/guides/history/table-history.md +657 -0
- package/dist/docs/guides/history/vacuum.md +26 -0
- package/dist/docs/guides/read-and-write/automatic-links.md +768 -0
- package/dist/docs/guides/read-and-write/batch.md +139 -0
- package/dist/docs/guides/read-and-write/import-notes.md +896 -0
- package/dist/docs/guides/read-and-write/joins-and-aggregates.md +251 -0
- package/dist/docs/guides/read-and-write/read-and-filter.md +536 -0
- package/dist/docs/guides/read-and-write/write-rows.md +573 -0
- package/dist/docs/guides/search/link-graph.md +241 -0
- package/dist/docs/guides/search/semantic.md +133 -0
- package/dist/docs/guides/search/text.md +142 -0
- package/dist/docs/guides/security/security-model.md +110 -0
- package/dist/docs/guides/security/tokens.md +60 -0
- package/dist/docs/guides/security/who-did-what.md +44 -0
- package/dist/docs/index.md +113 -0
- package/dist/docs/llms.txt +33 -0
- package/dist/docs/reference/cli.md +143 -0
- package/dist/docs/reference/compatibility.md +83 -0
- package/dist/docs/reference/errors.md +72 -0
- package/dist/docs/reference/glossary.md +161 -0
- package/dist/docs/reference/operations/auxiliary.md +116 -0
- package/dist/docs/reference/operations/history.md +168 -0
- package/dist/docs/reference/operations/queries.md +169 -0
- package/dist/docs/reference/operations/rows.md +185 -0
- package/dist/docs/reference/operations/schema.md +316 -0
- package/dist/docs/reference/operations.md +96 -0
- package/dist/docs/reference/settings.md +43 -0
- package/dist/docs/release-notes.md +188 -0
- package/dist/docs/start/connect-agents.md +204 -0
- package/dist/docs/start/how-it-works.md +349 -0
- package/dist/docs/start/quick-start.md +128 -0
- package/package.json +60 -15
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Send logs and traces to your observability backend
|
|
2
|
+
|
|
3
|
+
> Send the logs and traces of the engine, the MCP server and the apps to any OpenTelemetry backend, and find slow operations.
|
|
4
|
+
|
|
5
|
+
Every part of Datanotes sends logs and traces with OpenTelemetry, the vendor-neutral standard. Any backend with an OpenTelemetry Protocol (OTLP) endpoint can receive them. This page shows you how to:
|
|
6
|
+
|
|
7
|
+
* Point the engine, the Model Context Protocol (MCP) server and the apps at your backend
|
|
8
|
+
* Read the spans the engine sends, and what each one measures
|
|
9
|
+
* Follow one agent call across the MCP server and the engine
|
|
10
|
+
* Find the operations that are slow, and why
|
|
11
|
+
|
|
12
|
+
Datanotes has no vendor-specific code.
|
|
13
|
+
|
|
14
|
+
## What Datanotes sends and where
|
|
15
|
+
|
|
16
|
+
Datanotes sends two signals over HTTP:
|
|
17
|
+
|
|
18
|
+
* **Logs** to `your_endpoint/v1/logs`
|
|
19
|
+
* **Traces** to `your_endpoint/v1/traces`
|
|
20
|
+
|
|
21
|
+
It encodes them as `http/protobuf`, the OpenTelemetry default, or as `http/json`. Any OpenTelemetry Collector receives them, and so do these backends, among others:
|
|
22
|
+
|
|
23
|
+
* SigNoz
|
|
24
|
+
* Grafana: Alloy, Tempo, Loki or Grafana Cloud
|
|
25
|
+
* Elastic, Datadog, Honeycomb, New Relic and Dynatrace
|
|
26
|
+
|
|
27
|
+
Datanotes sends no metrics: your backend computes rates and latency percentiles from the spans. For a backend that accepts only gRPC, put an OpenTelemetry Collector in front of it. When the collector doesn’t answer, Datanotes counts the records and spans as lost and keeps working.
|
|
28
|
+
|
|
29
|
+
## Configure the export with environment variables
|
|
30
|
+
|
|
31
|
+
The engine, the MCP server and the apps all read the standard OpenTelemetry variables:
|
|
32
|
+
|
|
33
|
+
| Variable | Meaning |
|
|
34
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | The collector’s base address, for example `http://127.0.0.1:4318` |
|
|
36
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Or the full `…/v1/logs` address |
|
|
37
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Or the full `…/v1/traces` address |
|
|
38
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | `key=value,key2=value2`, with URL-encoded values: the API key or `Authorization` your backend asks for |
|
|
39
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (default) or `http/json` |
|
|
40
|
+
| `OTEL_SERVICE_NAME` | The service name; defaults to `datanotes`, `datanotes-mcp` (for `datanotes mcp`) or the app’s name |
|
|
41
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, for example `team=home` |
|
|
42
|
+
| `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` | `traceidratio` with a share from 0 to 1, `always_on` or `always_off`, plain or with the `parentbased_` prefix |
|
|
43
|
+
| `OTEL_SDK_DISABLED=true` | Turns all the export off |
|
|
44
|
+
| `OTEL_LOGS_EXPORTER=none` or `OTEL_TRACES_EXPORTER=none` | Turns off the logs or the traces only |
|
|
45
|
+
|
|
46
|
+
Datanotes also reads the `…_LOGS_…` and `…_TRACES_…` variants of the headers and protocol variables. Without an endpoint, it sends nothing.
|
|
47
|
+
|
|
48
|
+
## Configure the engine in its settings file
|
|
49
|
+
|
|
50
|
+
Instead of the variables, you can configure the engine in its settings file, `settings.json` in its store. `datanotes store path` shows where. A variable you set overrides the matching setting:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"telemetryEnabled": true,
|
|
55
|
+
"telemetryEndpoint": "http://127.0.0.1:4318",
|
|
56
|
+
"telemetryServiceName": "datanotes",
|
|
57
|
+
"telemetryEnvironment": "home",
|
|
58
|
+
"telemetryProtocol": "http/protobuf",
|
|
59
|
+
"telemetryHeaders": {},
|
|
60
|
+
"telemetryTraces": true,
|
|
61
|
+
"telemetryTraceSampleRatio": 1,
|
|
62
|
+
"telemetryMaxSpansPerTrace": 1000,
|
|
63
|
+
"telemetrySlowSpanMs": 0
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each setting does one thing:
|
|
68
|
+
|
|
69
|
+
* **`telemetryEnabled`** turns the export of logs and traces on.
|
|
70
|
+
* **`telemetryEnvironment`** names the environment, such as `dev` or `home`. The engine sends it as `deployment.environment.name` and as the older `deployment.environment`, which some backends group by.
|
|
71
|
+
* **`telemetryHeaders`** holds what your backend asks for, such as an API key. To keep a key out of the file, set `OTEL_EXPORTER_OTLP_HEADERS` instead.
|
|
72
|
+
* **`telemetryTraces`** sends the traces too. It is on by default: turning telemetry on gives you both signals.
|
|
73
|
+
* **`telemetryTraceSampleRatio`** keeps that share of new traces, from 0 to 1. The default, 1, keeps every trace. A trace that a caller started follows the caller’s choice.
|
|
74
|
+
* **`telemetryMaxSpansPerTrace`** caps the spans of one trace. The engine drops the rest and counts them in `datanotes.spans_dropped` on the trace’s first span.
|
|
75
|
+
* **`telemetrySlowSpanMs`** logs a request, an operation or a background job as a WARN when it lasts at least that many milliseconds. 0 turns it off.
|
|
76
|
+
|
|
77
|
+
The engine reads the settings again while it runs, so a change applies within a few seconds. With traces off, tracing costs the engine one check per instrumented call.
|
|
78
|
+
|
|
79
|
+
## What the engine logs
|
|
80
|
+
|
|
81
|
+
The engine logs these events:
|
|
82
|
+
|
|
83
|
+
* Every HTTP request and operation, with the route, duration, outcome and actor
|
|
84
|
+
* The tool calls MCP servers report, with the tool, duration, result size and client
|
|
85
|
+
* Refused requests
|
|
86
|
+
* The semantic index and git sync
|
|
87
|
+
* Errors
|
|
88
|
+
|
|
89
|
+
A record made during a traced request names its trace. The OTLP record carries the trace and span IDs, so your backend links the log to the trace. The log files carry them as `trace_id` and `span_id`. While a request’s trace is sent, its INFO records become DEBUG records: the span already says what they say.
|
|
90
|
+
|
|
91
|
+
Whatever the collector does, the engine also writes its records to files in `logs/`, in its store outside the vault. The `fileLogEnabled` setting turns these files on or off, and `logRetentionDays` sets how long the engine keeps them.
|
|
92
|
+
|
|
93
|
+
## The spans the engine sends
|
|
94
|
+
|
|
95
|
+
A trace is a tree of spans. Each span measures one piece of work and names its parent. A request to the engine gives this tree:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
POST /datanotes/operations/insert_row/ SERVER: the HTTP request
|
|
99
|
+
└── engine.op insert_row the operation
|
|
100
|
+
├── engine.schedule.wait a read waits for writes (only then)
|
|
101
|
+
├── engine.settle wait for the vault to settle
|
|
102
|
+
└── engine.insert_row the engine's work
|
|
103
|
+
├── engine.autolink links found in the text
|
|
104
|
+
├── engine.checks CHECKs and link rules
|
|
105
|
+
├── engine.lock.acquire wait for the write lock
|
|
106
|
+
└── engine.lock.hold work under the lock
|
|
107
|
+
├── engine.transaction_log hand edits logged first
|
|
108
|
+
├── engine.apply files written
|
|
109
|
+
│ └── engine.journal.append the undo entry
|
|
110
|
+
└── engine.transaction_log the operation's commit
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A read gives a similar tree under `engine.query.execute`:
|
|
114
|
+
|
|
115
|
+
* **`engine.query.view`** runs the stored query of a view.
|
|
116
|
+
* **`engine.query.plan`** resolves dates and `within` and compiles the filter.
|
|
117
|
+
* **`engine.query.scan`** reads every row of the table. Its `datanotes.query.plan` is `full_scan`: Datanotes has no indexes, and a file cache spares the reads of unchanged notes.
|
|
118
|
+
* **`engine.query.filter`**, **`engine.query.join`**, **`engine.query.group`**, **`engine.query.sort`** and **`engine.query.project`** follow, as the query needs them.
|
|
119
|
+
|
|
120
|
+
Other spans cover the rest of the engine’s time:
|
|
121
|
+
|
|
122
|
+
* **`engine.metastore.parse`** when `metastore.yaml` changed and the engine parses it again
|
|
123
|
+
|
|
124
|
+
* **`engine.history.read`** for a read as of a version or a time
|
|
125
|
+
|
|
126
|
+
* **`engine.graph.build`** when a graph operation rebuilds the link graph after a change. Calls that need it at the same time wait for that one build and count it in `datanotes.graph.build_waits`.
|
|
127
|
+
|
|
128
|
+
* **`engine.change_feed.record`** for the change feed’s events of an operation
|
|
129
|
+
|
|
130
|
+
* **`POST`** (CLIENT) for each webhook delivery, with `url.full`, `server.address` and `http.response.status_code`
|
|
131
|
+
|
|
132
|
+
* **`job <name>`** for background work. The jobs are:
|
|
133
|
+
|
|
134
|
+
* `job hand_edits_log`
|
|
135
|
+
* `job adopt_note`
|
|
136
|
+
* `job schema_reload`
|
|
137
|
+
* `job semantic_index`
|
|
138
|
+
* `job store_maintenance`
|
|
139
|
+
* `job change_feed_prune`
|
|
140
|
+
* `job change_feed_hand_edits`
|
|
141
|
+
* `job transaction_log_warmup`
|
|
142
|
+
* `job auto_recompute`
|
|
143
|
+
* `job inverse_links`
|
|
144
|
+
* `job datanotes_start`, the engine’s start
|
|
145
|
+
|
|
146
|
+
Background work is always a trace of its own, even when a request saw the change behind it. After a start, `job transaction_log_warmup` reads each table’s transaction log once, one table at a time, so a write waits for one table at most.
|
|
147
|
+
|
|
148
|
+
### Attributes of the operation span
|
|
149
|
+
|
|
150
|
+
The `engine.op <operation>` span follows the conventions of database spans. Most backends list these spans as database calls:
|
|
151
|
+
|
|
152
|
+
| Attribute | Meaning |
|
|
153
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
154
|
+
| `db.system.name`, `db.system` | `datanotes` |
|
|
155
|
+
| `db.operation.name` | The operation, such as `select_rows` |
|
|
156
|
+
| `db.collection.name` | The table or view the call names |
|
|
157
|
+
| `db.namespace` | The catalog and schema, when the call gives a full name |
|
|
158
|
+
| `db.query.summary` | The operation and its table, such as `select_rows task` |
|
|
159
|
+
| `db.response.returned_rows` | The rows the call returned |
|
|
160
|
+
| `datanotes.rows_scanned` | The rows the engine read to answer |
|
|
161
|
+
| `datanotes.notes_changed`, `datanotes.files_created` | What a write changed |
|
|
162
|
+
| `datanotes.migration_id` | The write’s journal entry, for `undo_migration` |
|
|
163
|
+
| `datanotes.dry_run` | Whether the call was a dry run |
|
|
164
|
+
| `datanotes.long_poll` | `true` for a `list_changes` call that waits for changes |
|
|
165
|
+
| `datanotes.schedule.class` | `read`, `write` or `free`: how [write priority](https://datanotes.dev/develop/concurrency/#reads-wait-for-writes) treats the call |
|
|
166
|
+
| `datanotes.schedule.wait_ms` | How long the call waited for writes before it started |
|
|
167
|
+
| `datanotes.schedule.released` | Why a waiting read started: `writes_done`, `max_wait` or `max_writes` |
|
|
168
|
+
| `datanotes.read_cache` | For a read: `hit` (answered from memory), `miss` or `not_kept` ([read cache](https://datanotes.dev/develop/concurrency/#repeated-reads-come-from-memory)) |
|
|
169
|
+
| `error.type` | Why the call failed or was refused |
|
|
170
|
+
|
|
171
|
+
The SERVER span adds `http.request.method`, `http.route`, `http.response.status_code`, `client.address` and `user_agent.original`. It also names the caller: `datanotes.token.name` holds the token’s name, never the token. `datanotes.client.harness` and `datanotes.client.model` hold what the agent says of itself.
|
|
172
|
+
|
|
173
|
+
### Calls to the vault
|
|
174
|
+
|
|
175
|
+
Reading a table of thousands of notes means thousands of file reads. The engine doesn’t make a span of each one. It adds them up on the span that made them and on its operation span:
|
|
176
|
+
|
|
177
|
+
* `datanotes.io.read_count`, `datanotes.io.read_chars` and `datanotes.io.read_ms`
|
|
178
|
+
* The same for `write`, `create`, `list`, `exists`, `rename` and the other calls
|
|
179
|
+
* `datanotes.cache.hits` and `datanotes.cache.misses` of the file cache
|
|
180
|
+
* `datanotes.cache.text_reads` and `datanotes.cache.text_source`: note texts the file cache read again, or took from the [vault index](https://datanotes.dev/administration/limits/#start-faster-with-the-vault-index)
|
|
181
|
+
* `datanotes.validate.rows` and `datanotes.validate.ms`, `datanotes.checks.rows` and `datanotes.checks.ms` for the checks run once per row
|
|
182
|
+
|
|
183
|
+
`_chars` counts characters, not bytes: the length of the text as JavaScript counts it, in UTF-16 code units. In English Markdown, characters and bytes are about the same. Accents, other alphabets and emoji take more bytes than characters. For `list`, `_chars` counts the files and folders listed. The calls that move no text count 0.
|
|
184
|
+
|
|
185
|
+
A single call that takes 20 ms or more gets a span of its own, `vault.<call>`, with its `file.path` and `datanotes.io.chars`.
|
|
186
|
+
|
|
187
|
+
### Errors and refusals
|
|
188
|
+
|
|
189
|
+
The engine tells failures and refusals apart:
|
|
190
|
+
|
|
191
|
+
* **A failure** is a bug, a disk error, a write that failed partway or a 5xx answer. Its span has status ERROR and an `exception` event with the type, message and stack.
|
|
192
|
+
* **A refusal** is a call the caller can fix: bad arguments, an unknown table, a broken CHECK. Its span keeps the status unset and sets `error.type`, such as `violation` or `TableNotFoundError`.
|
|
193
|
+
|
|
194
|
+
An empty answer to a `list_changes` poll sends no trace and no log line: services poll every few seconds.
|
|
195
|
+
|
|
196
|
+
## Follow one agent call across services
|
|
197
|
+
|
|
198
|
+
One agent call is one trace across the MCP server and the engine:
|
|
199
|
+
|
|
200
|
+
1. The MCP server makes a SERVER span, `tools/call <tool>`. It continues the client’s trace when the client sends `traceparent` in the call’s `_meta`.
|
|
201
|
+
2. Each request it sends to the engine is a CLIENT span, child of the tool’s span. The request carries that span as its `traceparent` header.
|
|
202
|
+
3. The engine’s SERVER span continues that trace, and its operation spans follow.
|
|
203
|
+
|
|
204
|
+
The tool span carries `mcp.method.name`, `gen_ai.operation.name` (`execute_tool`), `gen_ai.tool.name` and `mcp.session.id`. Find the agent call by its tool or session, open the trace, and you see where the time went in both processes.
|
|
205
|
+
|
|
206
|
+
The engine, the MCP server and the apps also log the same `session` and `tool` for each agent call. `session` is the first 16 characters of the MCP session ID.
|
|
207
|
+
|
|
208
|
+
## Find the slow operations
|
|
209
|
+
|
|
210
|
+
Your backend computes the latency of each span name from the traces. To find where the engine spends its time:
|
|
211
|
+
|
|
212
|
+
1. List the `engine.op <operation>` spans by duration, or open your backend’s database calls view. Leave out the spans with `datanotes.long_poll`: they wait on purpose.
|
|
213
|
+
2. Open a slow trace and compare its children. A long `engine.lock.acquire` means the call waited for another write. A long `engine.lock.hold` means the write itself was slow. A long `engine.schedule.wait` means a read waited for writes to end.
|
|
214
|
+
3. In a read, compare `engine.query.scan` with the rest. A slow scan with many `datanotes.cache.misses` reads many notes from disk. A slow scan with few misses works on a large table: narrow it with `where`, or split the table.
|
|
215
|
+
4. Look at the `datanotes.io.*_ms` attributes of the operation span. They show how much of its time went to the disk.
|
|
216
|
+
5. Set `telemetrySlowSpanMs` to log the slow calls as WARN records, and alert on them.
|
|
217
|
+
|
|
218
|
+
## Configure the MCP server and the apps
|
|
219
|
+
|
|
220
|
+
The MCP server and the apps read the same variables, and each has its own alternative:
|
|
221
|
+
|
|
222
|
+
* **`datanotes mcp`**: the `--otlp-endpoint your_endpoint` option, for its logs and traces
|
|
223
|
+
* **Apps**: an `otlp` block in the app’s configuration, `{"endpoint": "http://127.0.0.1:4318", "headers": {}, "serviceName": "…"}`
|
|
224
|
+
|
|
225
|
+
## Continue a trace from your own program
|
|
226
|
+
|
|
227
|
+
Your program continues its trace in the engine when it sends the W3C `traceparent` header, and `tracestate` when it has one. `datanotes/client` does it for you with two options of `DatanotesClient`:
|
|
228
|
+
|
|
229
|
+
* **`trace`** is the trace to continue: a `traceparent` value, a context, or a function that returns the current one.
|
|
230
|
+
* **`tracer`** makes a CLIENT span for each request, child of `trace`, and sends that span as the engine’s parent.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import {
|
|
234
|
+
DatanotesClient,
|
|
235
|
+
OtlpTraceExporter,
|
|
236
|
+
Tracer,
|
|
237
|
+
otlpTracesFromEnv,
|
|
238
|
+
} from "datanotes/client";
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
const otlp = otlpTracesFromEnv(process.env, { serviceName: "my-app" });
|
|
242
|
+
const exporter = otlp ? new OtlpTraceExporter(otlp) : null;
|
|
243
|
+
const tracer = new Tracer({ export: (s) => exporter?.export(s) });
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
const run = tracer.startSpan("import films", { kind: "server" });
|
|
247
|
+
const client = new DatanotesClient({
|
|
248
|
+
url: "http://127.0.0.1:27150",
|
|
249
|
+
token: process.env.DATANOTES_TOKEN ?? "",
|
|
250
|
+
tracer,
|
|
251
|
+
trace: run.context,
|
|
252
|
+
});
|
|
253
|
+
await client.call("insert_row", {
|
|
254
|
+
table: "film",
|
|
255
|
+
values: { title: "Arrival" },
|
|
256
|
+
});
|
|
257
|
+
run.end();
|
|
258
|
+
await exporter?.close();
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Without a tracer, pass the incoming header as it is. A call can also name its own trace:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
await client.call("select_rows", { table: "film" }, undefined, {
|
|
265
|
+
trace: request.headers.traceparent,
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Any HTTP client works too: send `traceparent: 00-<trace id>-<span id>-01` with the request.
|
|
270
|
+
|
|
271
|
+
## Send logs from your own program
|
|
272
|
+
|
|
273
|
+
`datanotes/client` exports the log exporter that Datanotes uses, so your own programs can log the same way:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { OtlpLogExporter, otlpFromEnv } from "datanotes/client";
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
const otlp = otlpFromEnv(process.env, { serviceName: "my-app" });
|
|
280
|
+
const log = otlp ? new OtlpLogExporter(otlp) : null;
|
|
281
|
+
log?.log("INFO", "imported films", { count: 8 });
|
|
282
|
+
await log?.close();
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`otlpFromEnv` reads the variables above and returns `null` when you set no endpoint or turn the export off. The program then logs nothing. Pass a span’s context as the fourth argument of `log` to link the record to that span. Call `close()` before the program exits to send the remaining records.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Read the docs offline
|
|
2
|
+
|
|
3
|
+
> The datanotes package ships these docs as Markdown for its version: agents read and search them with list_docs, read_docs and search_docs, programs through the engine, people from the package.
|
|
4
|
+
|
|
5
|
+
The `datanotes` package ships this documentation as Markdown: one file per page of the site in `dist/docs/`, with `llms.txt`. The pages match the installed version, and reading them needs no internet access.
|
|
6
|
+
|
|
7
|
+
## The agent reads the docs of its version
|
|
8
|
+
|
|
9
|
+
`datanotes mcp` serves these pages to agents. The agent looks up an operation or an example without internet access, and the pages always match the version it talks to. Three tools read them:
|
|
10
|
+
|
|
11
|
+
* **`list_docs`**: lists the pages, each with its path, title and description. `prefix` keeps one folder, such as `guides/`.
|
|
12
|
+
* **`read_docs`**: returns a page as Markdown, or one section of it by the anchor of its heading, such as `guides/read-and-write/read-and-filter#compare-values-with-operators`. It returns at most 20,000 characters at a time, and a longer text ends with how to read the rest.
|
|
13
|
+
* **`search_docs`**: finds the sections that match a few words, best first. Each hit has the path to pass to `read_docs` and a short excerpt.
|
|
14
|
+
|
|
15
|
+
The `Docs:` line of an error names the `read_docs` call for its section, so the agent reads it offline at once:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Docs: read_docs("guides/read-and-write/read-and-filter#compare-values-with-operators")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The server leaves out the web address, since its agents always have the docs tools. A program that uses `DatanotesClient` gets both: the address, then the `read_docs` call in parentheses. Your own MCP server with the docs tools drops the address with `offlineDocs` from `datanotes/mcp`.
|
|
22
|
+
|
|
23
|
+
An agent that asks for a page or section that doesn’t exist gets the closest ones in the hint. Reading a section instead of a whole page keeps the agent’s context small: `search_docs` finds the section, and `read_docs` returns only that.
|
|
24
|
+
|
|
25
|
+
## Search with required and excluded words
|
|
26
|
+
|
|
27
|
+
`search_docs` ranks the sections by the words of the query. A word matches in any form, so `rows` finds `row`. It also matches its synonyms, so `revert` finds `undo`. Words in a heading count most.
|
|
28
|
+
|
|
29
|
+
Three marks narrow the search:
|
|
30
|
+
|
|
31
|
+
* **`"a phrase"`**: the section must hold these words in a row, in any form, such as `"time travel"`
|
|
32
|
+
* **`+word`**: the section must hold the word, such as `+view`
|
|
33
|
+
* **`-word`**: the section must not hold the word, such as `-sql`; `-"a phrase"` leaves out a phrase
|
|
34
|
+
|
|
35
|
+
The other words only rank the sections. This query finds the sections on time travel that mention views and never SQL. Here, `db` is a [client](https://datanotes.dev/develop/client/) connected to the engine, as in [Build your own MCP server](https://datanotes.dev/develop/build-on-datanotes/#build-your-own-mcp-server):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { callDocsTool } from "datanotes/mcp";
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const hits = await callDocsTool(
|
|
42
|
+
"search_docs",
|
|
43
|
+
{ query: '"time travel" +view -sql' },
|
|
44
|
+
{ client: db },
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The marks filter the results, and never rank them. A section with every required word still ranks by all the words of the query. A word that starts with two dashes, such as `--vault`, is a plain word.
|
|
49
|
+
|
|
50
|
+
## Search by words and by meaning
|
|
51
|
+
|
|
52
|
+
When the engine has [semantic search](https://datanotes.dev/guides/search/semantic/) on, `search_docs` becomes a hybrid search. It ranks the sections twice:
|
|
53
|
+
|
|
54
|
+
* **By words**: the ranking above, made by the MCP server from its own copy of the docs
|
|
55
|
+
* **By meaning**: the engine embeds the query and ranks the sections by the similarity of their vectors
|
|
56
|
+
|
|
57
|
+
Reciprocal Rank Fusion merges the two rankings. Each section scores the sum of 1 / (60 + its rank) over the rankings that hold it. Only the first 50 sections of each ranking count. A section that both rankings place high comes first, ahead of one that a single ranking finds. The required and excluded words filter both rankings.
|
|
58
|
+
|
|
59
|
+
Each hit then ends with the rankings that found it: `(keyword, semantic)`, `(keyword)` or `(semantic)`. The last line of the answer says the search was hybrid.
|
|
60
|
+
|
|
61
|
+
This is the hybrid search of vector databases such as ChromaDB, with no service to add. The engine already has the embedding model of semantic search. It keeps the vectors of the docs in its store, in `semantic/docs.json`, apart from the index of your notes.
|
|
62
|
+
|
|
63
|
+
The engine builds that index in the background:
|
|
64
|
+
|
|
65
|
+
* **When it starts**: after it updates the index of your notes
|
|
66
|
+
* **When the docs change**: after an upgrade or a new docs build, the engine embeds again only the sections whose text changed
|
|
67
|
+
* **When the model changes**: another provider, address or model starts a new index
|
|
68
|
+
|
|
69
|
+
The version of the docs is a hash of their sections, so any change to a section counts. The docs are public: embedding them sends nothing from your vault. With a paid provider, the docs cost about as much to index as 700 short notes.
|
|
70
|
+
|
|
71
|
+
`search_docs` ranks by words alone, as without semantic search, in these cases:
|
|
72
|
+
|
|
73
|
+
* **No semantic search**: it’s off, or the engine is older than hybrid search
|
|
74
|
+
* **The index is being built**: the first search after a change starts it
|
|
75
|
+
* **The engine runs another version**: its docs differ from those of the MCP server
|
|
76
|
+
* **The engine doesn’t answer**: `search_docs` asks it again a minute later
|
|
77
|
+
|
|
78
|
+
## Read the docs from programs and editors
|
|
79
|
+
|
|
80
|
+
Programs and people read the same pages:
|
|
81
|
+
|
|
82
|
+
* **Programs**: the engine serves them, with a token like its other routes. `GET /datanotes/manual/` lists them, and `GET /datanotes/manual/guides/read-and-write/read-and-filter.md` returns one page. Add `?section=compare-values-with-operators` for one section.
|
|
83
|
+
* **Search by meaning**: `POST /datanotes/manual/search/` takes a `query` and returns the sections closest in meaning, with `ready: false` while the engine builds the index. Programs that use `datanotes/mcp` pass a `client` to `callDocsTool` to search like `datanotes mcp`.
|
|
84
|
+
* **People and editors**: the files themselves, in `node_modules/datanotes/dist/docs/`.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
curl -H "Authorization: Bearer your_token" \
|
|
88
|
+
http://127.0.0.1:27150/datanotes/manual/start/quick-start.md
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A program ranks the sections by meaning with the same [client](https://datanotes.dev/develop/client/):
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const ranked = await db.post("/manual/search/", {
|
|
95
|
+
query: "undo what an agent did",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
In a checkout of the repository, build the docs first with `cd docs && npm ci && npm run build`, which copies the pages to `dist/docs/`. Until then, the tools answer with that command.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# How agents work with your vault
|
|
2
|
+
|
|
3
|
+
> What an agent reads first, how it finds tables and views, why the engine validates its writes, and how the journal records and undoes what each agent did.
|
|
4
|
+
|
|
5
|
+
An agent connected through `datanotes mcp`, the Model Context Protocol (MCP) server of Datanotes, works with your vault the way a careful colleague works with a shared database. It reads the descriptions before it writes, writes only through operations the engine checks, and signs every change with its name in the journal. This page follows that path, from the instructions the agent receives to undoing what it did; the [prompts](https://datanotes.dev/guides/agents/prompts/) show it on concrete requests.
|
|
6
|
+
|
|
7
|
+
## The agent starts with the server’s instructions
|
|
8
|
+
|
|
9
|
+
When an MCP client connects, `datanotes mcp` sends instructions with its list of tools. The engine’s part is short:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Database of Markdown notes: tables defined in metastore.yaml, a row is a note whose front matter key `table` names its table, and each front matter key is a column. Use list_tables / describe_table first, select_rows to read, insert_row / update_row / update_rows / batch to write (validated, journaled, undo with undo_migration). Change the schema only with its operations (create_table, add_column, alter_column, create_view, ...); never edit metastore.yaml. Every table and column needs a `description` of what it holds, or it is refused: descriptions are how agents and people understand the schema. Fill those describe_table lists in `missing_descriptions` with alter_table / alter_column. A body you write links the notes it mentions by name (a one-word name only with a capital, inside a sentence): when a write returns ambiguous_mentions, read the candidate notes and link the right one with update_row and autolink: false, or leave the text. Read the docs of this version with search_docs and read_docs.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The tools are every [operation](https://datanotes.dev/reference/operations/) of the engine, plus three that [read the docs of the installed version](#the-agent-reads-the-docs-of-its-version). To give agents rules and tools of your own, serve [your own MCP server](https://datanotes.dev/develop/build-on-datanotes/#build-your-own-mcp-server) next to it. Each tool’s description says when to call it: `list_tables` asks the agent to call it first, and `add_column` asks the agent to read `describe_table` before it adds a column.
|
|
16
|
+
|
|
17
|
+
## The agent finds tables by their descriptions
|
|
18
|
+
|
|
19
|
+
Before it reads or writes, the agent matches your words against what the tables say about themselves:
|
|
20
|
+
|
|
21
|
+
* **`list_tables`**: every table and view, with its `description`, `aliases` and `tags`, so “my watchlist” leads to `film`, whose aliases include `watchlist`
|
|
22
|
+
* **`describe_table`**: the columns, each with its type, `description`, `aliases`, limits and, for an enum, the meaning of each value, plus the table’s row count
|
|
23
|
+
|
|
24
|
+
The meanings come from `value_descriptions`, which you give when you create the column. On the film diary, `describe_table` answers this for `status`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "status",
|
|
29
|
+
"type": "enum",
|
|
30
|
+
"values": ["to watch", "watched"],
|
|
31
|
+
"value_descriptions": {
|
|
32
|
+
"to watch": "on my watchlist, not seen yet",
|
|
33
|
+
"watched": "I saw it at least once"
|
|
34
|
+
},
|
|
35
|
+
"ordered": true,
|
|
36
|
+
"default": "to watch"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every table and every column has a description, because the engine refuses one without. Agents and people understand the schema through them, so write each for a reader who never saw your notes. Good aliases help too: the better both are, the less the agent guesses. You set them with `create_table`, `add_column`, `alter_table`, `alter_column` and, for catalogs and schemas, `comment_on`.
|
|
41
|
+
|
|
42
|
+
A table created before descriptions were required may lack some. `describe_table` lists them in `missing_descriptions`, and the agent fills them with `alter_table` and `alter_column`. [Describe every table and column](https://datanotes.dev/guides/database-objects/tables/#describe-every-table-and-column) shows the rule in use.
|
|
43
|
+
|
|
44
|
+
## The agent reads views first
|
|
45
|
+
|
|
46
|
+
A view is a stored query with a name, a description and aliases, such as `vw_to_watch`: the films whose status is `to watch`, shortest first. `list_tables` returns views next to tables, and `select_rows` reads a view like a table and can narrow it further. When a view answers the question, the agent reads it instead of rebuilding the filter, so the answer follows your definition of “to watch” and not the agent’s. [Views](https://datanotes.dev/guides/database-objects/views/) shows views with joins, groups and filters on the note they’re shown for.
|
|
47
|
+
|
|
48
|
+
## The agent writes only through checked operations
|
|
49
|
+
|
|
50
|
+
Rows change through `insert_row`, `update_row`, `update_rows`, `delete_rows` and `batch`; the schema changes through operations such as `create_table`, `add_column` and `add_constraint`. The engine checks every one against the table’s column types, limits, links and CHECK constraints, and writes nothing when a check fails. Three arguments make an agent’s writes safer:
|
|
51
|
+
|
|
52
|
+
* **`dry_run`**: reports what would change, without writing
|
|
53
|
+
* **`batch`**: a group of writes that succeed or fail together, and that you undo as one
|
|
54
|
+
* **`expect`** on `update_row`: writes only if the row still has the values the agent read, so it doesn’t overwrite an edit you made in the meantime
|
|
55
|
+
|
|
56
|
+
The agent never edits `metastore.yaml`: a hand edit skips validation and has no undo. [Concurrent writes](https://datanotes.dev/develop/concurrency/) explains what happens when agents, programs and people write at the same time.
|
|
57
|
+
|
|
58
|
+
## A refusal tells the agent what to fix
|
|
59
|
+
|
|
60
|
+
A refused write names the note, the column or rule, and the values the engine accepts. It ends with a hint on how to fix it and a link to the page that shows the fix. For a film with the status `seen`, the engine answers:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
insert_row refused — nothing was written:
|
|
64
|
+
invalid datanotes/main/public/film/Sunrise.md: status must be one of: to watch (on my watchlist, not seen yet); watched (I saw it at least once)
|
|
65
|
+
Hint: Use one of the allowed values listed in the message, written exactly; describe_table shows what each one means. To add a value, alter_column the enum. Then call insert_row again (dry_run: true checks without writing).
|
|
66
|
+
Docs: read_docs("guides/database-objects/columns#describe-what-each-enum-value-means")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The agent can correct the value and try again, or ask you. Every other error follows the same pattern: a misspelled table or column gets the closest real name, a wrong filter operator gets the one meant, and a tool argument the tool doesn’t take gets the tool’s arguments. [Read the engine’s errors](https://datanotes.dev/guides/agents/reading-errors/) shows them all. Before it writes a link, the agent can call `list_link_targets`, which returns the rows the link column accepts, such as the genres a film may link to. Over HTTP, a refused write comes back with status 422 and the reasons in the body.
|
|
70
|
+
|
|
71
|
+
## The agent reads the docs of its version
|
|
72
|
+
|
|
73
|
+
`datanotes mcp` also serves this documentation, read from the installed `datanotes` package, with three tools: `list_docs`, `read_docs` and `search_docs`. The agent looks up an operation or an example without internet access, and the pages always match the version it talks to. [Read the docs offline](https://datanotes.dev/guides/agents/docs-offline/) shows the tools.
|
|
74
|
+
|
|
75
|
+
## Every write names the agent that made it
|
|
76
|
+
|
|
77
|
+
The engine records an actor with every change. Through `datanotes mcp`, the actor comes from the MCP connection:
|
|
78
|
+
|
|
79
|
+
* **`harness`** and **`harness_version`**: the client’s name and version from the MCP handshake, such as `claude-code`
|
|
80
|
+
* **`session`**: the MCP session, shortened to 16 characters
|
|
81
|
+
* **`tool`**: the tool the agent called
|
|
82
|
+
* **`auth`**: `owner`, or the token’s name for a [named token](https://datanotes.dev/start/connect-agents/#give-each-agent-its-own-token)
|
|
83
|
+
* **`model`**: the model, when an MCP server reports it with `POST /datanotes/sessions/identify/`
|
|
84
|
+
|
|
85
|
+
`describe_history` shows the actor of each commit:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"table": "main.public.film",
|
|
90
|
+
"version": 10,
|
|
91
|
+
"operation": "UPDATE",
|
|
92
|
+
"migration_id": "20261009T215306-update_rows-update-film",
|
|
93
|
+
"actor": {
|
|
94
|
+
"session": "5d8c0456-3b15-40",
|
|
95
|
+
"harness": "claude-code",
|
|
96
|
+
"harness_version": "2.5.0",
|
|
97
|
+
"tool": "batch",
|
|
98
|
+
"auth": "owner"
|
|
99
|
+
},
|
|
100
|
+
"batch": "20261009T215306-batch-log-the-screening-of-metropoli"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Three operations read this record:
|
|
105
|
+
|
|
106
|
+
* **`list_migrations`**: the journal, newest first, each operation with `by`, the agent’s name
|
|
107
|
+
* **`describe_history`**: the commits of one table or of every table, with the full actor; it needs history on
|
|
108
|
+
* **`list_changes`**: every row change with its actor, and with `kinds: ["tool_call"]` every tool call the MCP server reported, reads included
|
|
109
|
+
|
|
110
|
+
A script records its own actor with the `X-Datanotes-Actor` header or the client’s `actor` option, as the [HTTP API guide](https://datanotes.dev/develop/http-api/#record-who-makes-each-write) shows.
|
|
111
|
+
|
|
112
|
+
## Undo what an agent did
|
|
113
|
+
|
|
114
|
+
Because the journal records every write, you can take back one operation or a whole run:
|
|
115
|
+
|
|
116
|
+
* **One operation**: `undo_migration` with its `migration_id`; for a `batch`, it reverts every write in it
|
|
117
|
+
* **A whole run**: `restore_database` with `before_migration` set to the agent’s first operation or batch; every table with history goes back to its state before that operation (run it with `dry_run` first)
|
|
118
|
+
* **One table**: `restore_table` to a version or a time
|
|
119
|
+
|
|
120
|
+
The journal records an undo too, so you can undo the undo. The engine refuses an undo that would overwrite a note you edited since, and lists the conflicts. [Restore a table or the database](https://datanotes.dev/guides/history/restore/) shows each operation with the engine’s answers, and the [prompts](https://datanotes.dev/guides/agents/prompts/#safety-and-history-prompts) show how to ask for them.
|
|
121
|
+
|
|
122
|
+
## Give each agent only what it needs
|
|
123
|
+
|
|
124
|
+
The owner token can do everything. A [named token](https://datanotes.dev/guides/security/tokens/) limits an agent to the tables in `write`, to schema changes only with `ddl`, and to configuration, undo and restore only with `maintain`. The journal then names the token with each of its writes. [Connect agents](https://datanotes.dev/start/connect-agents/) shows how to create one and pass it to an MCP client.
|