@terreno/mcp 57.10.1 → 57.12.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/dist/docs/upgrades/57.11.0.md +80 -0
- package/dist/docs/upgrades/57.12.0.md +89 -0
- package/dist/docs/versioned/next/explanation/ai-workflows.md +4 -3
- package/dist/docs/versioned/next/explanation/durable-agent-harness.md +17 -4
- package/dist/docs/versioned/next/explanation/local-first-data.md +1 -0
- package/dist/docs/versioned/next/explanation/roadmap-process.md +4 -2
- package/dist/docs/versioned/next/how-to/README.md +1 -0
- package/dist/docs/versioned/next/how-to/call-external-apis.md +1 -1
- package/dist/docs/versioned/next/how-to/circleci.md +6 -2
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +5 -6
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
- package/dist/docs/versioned/next/how-to/preview-demo.md +7 -3
- package/dist/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
- package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +1 -1
- package/dist/docs/versioned/next/reference/ai-harness.md +138 -17
- package/dist/docs/versioned/next/reference/ai.md +33 -4
- package/dist/docs/versioned/next/reference/api.md +64 -2
- package/dist/docs/versioned/next/reference/environment-variables.md +1 -1
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
- package/dist/docs/versioned/next/reference/syncdb.md +156 -6
- package/dist/docs/versioned/next/reference/ui.md +14 -5
- package/dist/docs/versioned/next/tasks/durable-agent-harness.md +4 -2
- package/package.json +3 -3
- package/src/docs/upgrades/57.11.0.md +80 -0
- package/src/docs/upgrades/57.12.0.md +89 -0
- package/src/docs/versioned/next/explanation/ai-workflows.md +4 -3
- package/src/docs/versioned/next/explanation/durable-agent-harness.md +17 -4
- package/src/docs/versioned/next/explanation/local-first-data.md +1 -0
- package/src/docs/versioned/next/explanation/roadmap-process.md +4 -2
- package/src/docs/versioned/next/how-to/README.md +1 -0
- package/src/docs/versioned/next/how-to/call-external-apis.md +1 -1
- package/src/docs/versioned/next/how-to/circleci.md +6 -2
- package/src/docs/versioned/next/how-to/github-actions-ci.md +5 -6
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
- package/src/docs/versioned/next/how-to/preview-demo.md +7 -3
- package/src/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
- package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +1 -1
- package/src/docs/versioned/next/reference/ai-harness.md +138 -17
- package/src/docs/versioned/next/reference/ai.md +33 -4
- package/src/docs/versioned/next/reference/api.md +64 -2
- package/src/docs/versioned/next/reference/environment-variables.md +1 -1
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
- package/src/docs/versioned/next/reference/syncdb.md +156 -6
- package/src/docs/versioned/next/reference/ui.md +14 -5
- package/src/docs/versioned/next/tasks/durable-agent-harness.md +4 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Upgrading to 57.11.0
|
|
2
|
+
|
|
3
|
+
**Action required:** Yes
|
|
4
|
+
**Affected packages:** @terreno/ai, @terreno/api
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
`InProcessRunner` now runs up to eight harness tasks at once. List filters on a field that is not in `queryFields` return 400 before request validation, so that filter can no longer be stripped and ignored. Slack channel helpers and syncdb query windows are opt-in.
|
|
9
|
+
|
|
10
|
+
## Breaking changes
|
|
11
|
+
|
|
12
|
+
### InProcessRunner runs tasks in parallel
|
|
13
|
+
|
|
14
|
+
**What changed**
|
|
15
|
+
|
|
16
|
+
`new InProcessRunner()` runs up to 8 claimed tasks at once. Each task has its own lease. `stop()` waits for every in-flight task. Turns inside one conversation stay serial. Model calls run in parallel, so provider rate limits, cost, and stream-counter transaction retries all go up.
|
|
17
|
+
|
|
18
|
+
**Why**
|
|
19
|
+
|
|
20
|
+
A single slow model call was blocking every other task on that process.
|
|
21
|
+
|
|
22
|
+
**Migration**
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Before: one claimed task at a time
|
|
26
|
+
const runner = new InProcessRunner();
|
|
27
|
+
|
|
28
|
+
// After: keep that behavior
|
|
29
|
+
const runner = new InProcessRunner({concurrency: 1});
|
|
30
|
+
|
|
31
|
+
// Or accept the new default (8)
|
|
32
|
+
const runner = new InProcessRunner();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### List filters outside queryFields are no longer ignored
|
|
36
|
+
|
|
37
|
+
**What changed**
|
|
38
|
+
|
|
39
|
+
`GET` list on `modelRouter` checks every filter against `queryFields` before OpenAPI query validation. A field that is not listed returns 400 `query-param-not-allowed`. The body names the field, the model, and `meta.allowedQueryFields`. `@terreno/syncdb` raises `QueryFieldNotAllowedError` for the same response. Pagination params `limit`, `page`, and `sort` are unchanged.
|
|
40
|
+
|
|
41
|
+
**Why**
|
|
42
|
+
|
|
43
|
+
Query validation could strip an unknown filter and still return 200, so the client thought the filter applied.
|
|
44
|
+
|
|
45
|
+
**Migration**
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Before: ?title=ship could be stripped and the list still returned 200
|
|
49
|
+
modelRouter("/todos", Todo, {queryFields: ["completed", "ownerId"]});
|
|
50
|
+
|
|
51
|
+
// After: add the field, or stop sending it
|
|
52
|
+
modelRouter("/todos", Todo, {queryFields: ["completed", "ownerId", "title"]});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A 400 looks like:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"status": 400,
|
|
60
|
+
"code": "query-param-not-allowed",
|
|
61
|
+
"title": "Query parameter not allowed",
|
|
62
|
+
"meta": {"queryParam": "title", "model": "Todo", "allowedQueryFields": ["completed", "ownerId"]}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Deprecations
|
|
67
|
+
|
|
68
|
+
None
|
|
69
|
+
|
|
70
|
+
## New capabilities
|
|
71
|
+
|
|
72
|
+
- `JobsRunner` from `@terreno/ai/harness/jobsRunner` runs each harness phase as a `@terreno/jobs` job (`terreno.harness.phase`) so more than one instance can share the work. `@terreno/jobs` is an optional peer. `HarnessTask.claims` defaults to 0 and needs no migration. See `docs/how-to/run-harness-on-multiple-instances.md`.
|
|
73
|
+
- `@terreno/api` Slack helpers use the existing `SLACK_BOT_TOKEN`: `createSlackPrivateChannel`, `inviteSlackUsersToChannel`, `postSlackMessage`, and `findSlackChannelByName`. The bot needs `groups:write`, `groups:write.invites`, `channels:write.invites`, `chat:write`, `channels:read`, and `groups:read`. A taken channel name returns 409.
|
|
74
|
+
- `@terreno/syncdb` query windows: set `queryCollections`, then use `client.queryWindows` or `useWindowQuery` (and generated `use{Collection}Window` hooks) for server-filtered, paged lists. `useQuery` and `useEntityIds` accept the same Mongo-style `where`, string sort, and `limit` for local queries. `fullSync: true` adds windows without turning off snapshot sync.
|
|
75
|
+
|
|
76
|
+
## Verification
|
|
77
|
+
|
|
78
|
+
- Apps that construct `InProcessRunner` with no options: confirm parallel model calls are acceptable, or pass `{concurrency: 1}`.
|
|
79
|
+
- Hit one list route with a filter field that is not in `queryFields` and confirm 400 `query-param-not-allowed` instead of a 200 that ignored the filter.
|
|
80
|
+
- `bun run compile` and `bun run lint` at the app root.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Upgrading to 57.12.0
|
|
2
|
+
|
|
3
|
+
**Action required:** Yes
|
|
4
|
+
**Affected packages:** @terreno/ai
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
Harness tasks can record a trace scope, tags, and prompt versions, and `@terreno/ai` publishes official subpath exports. The chat `Project` model registers on first use instead of at import, and harness task HTTP responses no longer include the lease fencing token.
|
|
9
|
+
|
|
10
|
+
## Breaking changes
|
|
11
|
+
|
|
12
|
+
### `Project` is no longer registered on import
|
|
13
|
+
|
|
14
|
+
**What changed**
|
|
15
|
+
|
|
16
|
+
Importing `@terreno/ai` does not register mongoose model `Project`. The exported `Project` is a lazy proxy. The first property access, `new Project(...)`, or `getProjectModel()` registers the model. Statics and `instanceof Project` still work. These do not:
|
|
17
|
+
|
|
18
|
+
- `Project === mongoose.models.Project` (and `Project === mongoose.model("Project")`)
|
|
19
|
+
- `Object.keys(Project)` (always empty)
|
|
20
|
+
- reading `Project.prototype` (throws)
|
|
21
|
+
|
|
22
|
+
`new AiApp()` still mounts `/gpt/projects` and registers the model when those routes load. `new AiApp({projects: false})` skips those routes.
|
|
23
|
+
|
|
24
|
+
**Why**
|
|
25
|
+
|
|
26
|
+
An app that imports `@terreno/ai` and owns its own model named `Project` was hitting a mongoose overwrite error at import time.
|
|
27
|
+
|
|
28
|
+
**Migration**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// Before
|
|
32
|
+
import {Project} from "@terreno/ai";
|
|
33
|
+
|
|
34
|
+
const project = await Project.findExactlyOne({_id: id});
|
|
35
|
+
if (mongoose.models.Project === Project) {
|
|
36
|
+
// identity check
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// After
|
|
40
|
+
import {getProjectModel} from "@terreno/ai";
|
|
41
|
+
|
|
42
|
+
const Project = getProjectModel();
|
|
43
|
+
const project = await Project.findExactlyOne({_id: id});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To keep a separate app model named `Project`, do not call `getProjectModel()` or touch the `Project` export, and skip the AI project routes:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
new AiApp({aiService, projects: false});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Harness task HTTP responses omit `lease`
|
|
53
|
+
|
|
54
|
+
**What changed**
|
|
55
|
+
|
|
56
|
+
`GET /harness/tasks/:id` no longer includes `lease`. The OpenAPI schema for that response omits it too. Collection list is disabled (`list: []`, 405). The token is still stored on the task document for runners.
|
|
57
|
+
|
|
58
|
+
**Why**
|
|
59
|
+
|
|
60
|
+
`lease.token` is the fencing token that decides which runner may commit. It is not a client field.
|
|
61
|
+
|
|
62
|
+
**Migration**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Before
|
|
66
|
+
const token = task.lease?.token;
|
|
67
|
+
|
|
68
|
+
// After
|
|
69
|
+
// Do not read lease from the HTTP task. Runners load it on the server.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Regenerate the OpenAPI SDK after upgrading `@terreno/ai` so generated task types drop `lease`.
|
|
73
|
+
|
|
74
|
+
## Deprecations
|
|
75
|
+
|
|
76
|
+
`Project` from `@terreno/ai` still works for `find`, `create`, `new`, and `instanceof`. It is deprecated. Call `getProjectModel()` and use that model. A later major can remove the proxy.
|
|
77
|
+
|
|
78
|
+
## New capabilities
|
|
79
|
+
|
|
80
|
+
- `harness.createTask(def, input, {trace: {scope, tags}, prompts})` writes a scope (tenant, workspace), tags, and prompt versions onto the task's `ObsTrace`. Child tasks and subagents share that trace. The admin trace list filters with `?scope=`.
|
|
81
|
+
- `rt.runAgent` accepts `prompts` and a registry `PromptVersionRef` as `instructions`. It runs `body` and records the version on the trace.
|
|
82
|
+
- Subpath exports: `@terreno/ai/admin`, `@terreno/ai/harness/agentLoop`, `@terreno/ai/harness/commit`, `@terreno/ai/harness/events`, `@terreno/ai/harness/internalRuntime`, `@terreno/ai/observability/observabilityApp`, `@terreno/ai/observability/localPlugin`, `@terreno/ai/observability/promptStore`.
|
|
83
|
+
|
|
84
|
+
## Verification
|
|
85
|
+
|
|
86
|
+
- Code that imported `Project` from `@terreno/ai`: switch identity checks and `mongoose.models.Project` reads to `getProjectModel()`.
|
|
87
|
+
- An app that owns a model named `Project`: pass `projects: false` and do not call `getProjectModel()`.
|
|
88
|
+
- Harness clients: confirm `GET /harness/tasks/:id` JSON has no `lease`, then regenerate the SDK.
|
|
89
|
+
- `bun run compile` and `bun run lint` at the app root.
|
|
@@ -49,9 +49,10 @@ Maintains centralized agent guidance and generates the matching configuration fo
|
|
|
49
49
|
|
|
50
50
|
Rulesync generates native hooks from `.rulesync/hooks.json`. Biome checks changed files
|
|
51
51
|
after edits and staged files before commits. At agent stop,
|
|
52
|
-
`.rulesync/hooks/quality-check.sh` runs
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
`.rulesync/hooks/quality-check.sh` runs `bun install --frozen-lockfile` when
|
|
53
|
+
`node_modules/typescript` or any workspace package link is missing, then runs lint,
|
|
54
|
+
TypeScript compilation, Knip, and dependency-cruiser. It keeps stdout machine-readable
|
|
55
|
+
for each host and prevents retry-triggered Stop hooks from rerunning the commands.
|
|
55
56
|
|
|
56
57
|
Existing findings are ratcheted so the checks reject new debt without requiring unrelated
|
|
57
58
|
cleanup. See [Static analysis](static-analysis.md).
|
|
@@ -47,8 +47,11 @@ agents, tools, and conversations (see [The agent loop](#the-agent-loop)), subage
|
|
|
47
47
|
extensions, hooks, and memos (see [Extensions, hooks, and memos](#extensions-hooks-and-memos)),
|
|
48
48
|
events, waits, and approvals with the admin inbox, the SSE event stream, and version
|
|
49
49
|
pinning. The example backend's `clinic.intakeSummary` runs all of it end to end, including
|
|
50
|
-
a crash drill: [Build a durable workflow](../how-to/build-a-durable-workflow.md).
|
|
51
|
-
|
|
50
|
+
a crash drill: [Build a durable workflow](../how-to/build-a-durable-workflow.md).
|
|
51
|
+
|
|
52
|
+
Phase 2 so far: `InProcessRunner` concurrency (up to 8 tasks at once by
|
|
53
|
+
default) and the `JobsRunner`, which runs each phase as a `@terreno/jobs` job across many
|
|
54
|
+
instances. The Runs admin screen is still to come.
|
|
52
55
|
|
|
53
56
|
## Why the audit span shares the checkpoint transaction
|
|
54
57
|
|
|
@@ -237,8 +240,18 @@ tasks. Switching runners needs no data migration.
|
|
|
237
240
|
|
|
238
241
|
| Runner | Status | Ownership |
|
|
239
242
|
| --- | --- | --- |
|
|
240
|
-
| `InProcessRunner` | Shipped | One process holds the `HarnessOwner` lease and
|
|
241
|
-
| `JobsRunner` |
|
|
243
|
+
| `InProcessRunner` | Shipped | One process holds the `HarnessOwner` lease and runs up to `concurrency` (default 8) claimed tasks at once, each under its own task lease; others wait on standby and take over on lease expiry. |
|
|
244
|
+
| `JobsRunner` | Shipped | Every instance dispatches runnable tasks as `terreno.harness.phase` jobs; a job claims its task, runs one phase, and hands the task back. The task lease is the authority. |
|
|
245
|
+
|
|
246
|
+
Why the jobs runner runs **one** phase per job: a phase is the unit that commits, so it is
|
|
247
|
+
the natural unit of work to spread across instances and to bound by a request or job
|
|
248
|
+
timeout. Why the task lease and not the job lock decides who commits: a jobs lock can
|
|
249
|
+
expire while a long phase still runs, and a queue can deliver a job twice. Both are
|
|
250
|
+
harmless here, because the claim is an atomic fenced update and every commit checks the
|
|
251
|
+
lease token. Why the job key carries `claims`: `taskId:phase:attempt` alone repeats when a
|
|
252
|
+
task returns to the same phase without a failure (an event wake or a crash replay), and a
|
|
253
|
+
permanent idempotency key would then swallow the new visit. `claims` changes on every
|
|
254
|
+
claim, so each visit gets one job.
|
|
242
255
|
|
|
243
256
|
## Version pinning
|
|
244
257
|
|
|
@@ -39,6 +39,7 @@ Terreno 57.
|
|
|
39
39
|
3. **Encryption at rest (web)** — Web persistence uses AES-GCM via the default `createServerKeyProvider`; understand key fetch and `onDecryptFailure` for support flows.
|
|
40
40
|
4. **Key lifecycle on logout** — Call `syncDb.stop()` and consider `wipeLocalData` when the authenticated user changes so one account does not read another's local store.
|
|
41
41
|
5. **Backend sync registration** — Every synced collection needs `syncPlugin`, `isDeletedPlugin`, and a `sync` block on `modelRouter`, plus `SyncApp` and `RealtimeApp` on the server. Optional `sync.adminBroadcast: true` also emits `sync:delta` to `{collection}|admin`. Admin clients subscribe with `mode: "window"` (`createSyncDb({windowCollections})`) so they do not snapshot or reconcile that collection; they call `hydrateWindow` (REST membership ids + `GET /sync/entities`). Pass the same `accessControl` into `SyncApp` that `AdminApp` uses so `{collection}|admin` requires `admin:access`. `AdminApp.register` also installs that model's admin list/read/`queryFilter` on the admin window so hydrate and live `|admin` deltas match `/admin` REST. Without RBAC, `user.admin` is enough for the shell gate. Live deltas on that stream apply only to ids already in the window; Refresh is a REST re-query plus `hydrateWindow`.
|
|
42
|
+
6. **Collections too large to sync whole** — List them in `createSyncDb({queryCollections})`. They skip snapshot bootstrap; screens load server-filtered *query windows* (`useWindowQuery`) from the `modelRouter` list endpoint. A window stores only membership (ordered ids); rows live once in the shared entity table. Overlapping windows therefore share rows — one delta updates both views — while each view lists only its own query's members, plus cached rows that newly match its `where` inside the loaded range. Live deltas still arrive on the user's normal streams; windows on screen refetch on reconnect to cover deltas missed offline.
|
|
42
43
|
|
|
43
44
|
## Tradeoffs
|
|
44
45
|
|
|
@@ -402,7 +402,7 @@ do not claim completion until done.
|
|
|
402
402
|
| Workflow | Trigger | Purpose |
|
|
403
403
|
| -------- | ------- | ------- |
|
|
404
404
|
| [`.github/workflows/triage.yml`](https://github.com/TerrenoLabs/terreno/blob/master/.github/workflows/triage.yml) | Issue opened | `status:needs-triage` + `area:*` from package dropdown + `type:*` from Kind when present; `area:*` / `type:*` labels the issue was opened with win, so `roadmap:sync` issues are not asked for an area |
|
|
405
|
-
| [`.github/workflows/roadmap-generate.yml`](https://github.com/TerrenoLabs/terreno/blob/master/.github/workflows/roadmap-generate.yml) | Daily + manual | `roadmap:sync --check` for board drift, then regenerate `ROADMAP.md` from the board and open a pull request when
|
|
405
|
+
| [`.github/workflows/roadmap-generate.yml`](https://github.com/TerrenoLabs/terreno/blob/master/.github/workflows/roadmap-generate.yml) | Daily + manual | `roadmap:sync --check` for board drift, then regenerate `ROADMAP.md` from the board and open a pull request when roadmap content changes. A new **Last updated** timestamp alone does not open a pull request |
|
|
406
406
|
| [`.github/workflows/roadmap-sync.yml`](https://github.com/TerrenoLabs/terreno/blob/master/.github/workflows/roadmap-sync.yml) | Taxonomy files change on `master` + manual | Apply labels and reconcile the board's fields and items |
|
|
407
407
|
| [`.github/workflows/roadmap-reconcile.yml`](https://github.com/TerrenoLabs/terreno/blob/master/.github/workflows/roadmap-reconcile.yml) | IP or task files change on `master` + manual | Advance status from IP headers, add an entry for every new IP, open its tracking issue, push to the board, regenerate `ROADMAP.md` |
|
|
408
408
|
All three share the `roadmap` concurrency group. They write the same board, and interleaving
|
|
@@ -495,4 +495,6 @@ GITHUB_TOKEN=$(gh auth token) TERRENO_PROJECT_NUMBER=... bun run roadmap:generat
|
|
|
495
495
|
```
|
|
496
496
|
|
|
497
497
|
The generator exits non-zero when the project cannot be read, so a bad project number or a
|
|
498
|
-
token without `read:project` fails loudly instead of writing an empty `ROADMAP.md`.
|
|
498
|
+
token without `read:project` fails loudly instead of writing an empty `ROADMAP.md`. When the
|
|
499
|
+
rendered list matches the file already on disk, it keeps that file's **Last updated** stamp
|
|
500
|
+
and does not write. The daily workflow then sees a clean diff and does not open a pull request.
|
|
@@ -37,6 +37,7 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
|
|
|
37
37
|
- [Develop an AI feature](ai-feature-development.md) — Dataset → prompt → evaluators → experiment → production label → live traces (SOP)
|
|
38
38
|
- [Observe LLM calls](observe-llm-calls.md) — Register ObservabilityApp, primaries, price map, sampleRate
|
|
39
39
|
- [Build a durable workflow](build-a-durable-workflow.md) — Phased harness task with an agent, a human sign-off, and a once-only side effect (`clinic.intakeSummary`)
|
|
40
|
+
- [Run the harness on multiple instances](run-harness-on-multiple-instances.md) — `JobsRunner`: harness phases as `@terreno/jobs` jobs on every instance
|
|
40
41
|
- [Ship a new task version](ship-a-new-task-version.md) — Change a durable harness task without stranding in-flight runs
|
|
41
42
|
- [GitHub issue lifecycle](github-issue-lifecycle.md) — Create pick-ready issues, Pick ⇄ Roast, or unattended `status:ready-for-dev` pickup
|
|
42
43
|
- [Call external APIs](call-external-apis.md) — Authenticated HTTP client, retries, and error normalization for third-party integrations
|
|
@@ -94,7 +94,7 @@ Note the hook's output classification also feeds the client's retry decision —
|
|
|
94
94
|
|
|
95
95
|
- **Your own backend's routes** — use the generated `@terreno/rtk` SDK (frontend) or call the function directly (backend).
|
|
96
96
|
- **SDK-managed transport** (Twilio, googleapis) — don't fight the SDK's HTTP layer; adopt only `normalizeApiError`/`withApiErrorHandling` for consistent failure handling.
|
|
97
|
-
- **Webhook notifiers** — `sendToSlack`/`sendToGoogleChat`/`sendToZoom`
|
|
97
|
+
- **Webhook notifiers** — `sendToSlack`/`sendToGoogleChat`/`sendToZoom` post to a fixed webhook. Slack channel creation, invites, and bot posts use `createSlackPrivateChannel`, `inviteSlackUsersToChannel`, and `postSlackMessage` with `SLACK_BOT_TOKEN`. See [Webhooks & Notifications](../reference/api.md#webhooks--notifications).
|
|
98
98
|
|
|
99
99
|
## Related
|
|
100
100
|
|
|
@@ -254,8 +254,12 @@ the review script so a PR cannot rewrite the reviewer.
|
|
|
254
254
|
Netlify and GCP jobs **validate their context as the first step** and fail,
|
|
255
255
|
before checkout or `bun install`, listing the missing variables. Preview jobs
|
|
256
256
|
halt earlier on fork PRs, where CircleCI withholds contexts. Docker Layer
|
|
257
|
-
Caching is off (200 credits per job). The docs Netlify target
|
|
258
|
-
|
|
257
|
+
Caching is off (200 credits per job). The docs Netlify target builds only the
|
|
258
|
+
current docs tree, disables Docusaurus minification, and pins SSG to one worker
|
|
259
|
+
thread so the build peaks near 3 GB and fits the 4 GB `medium` executor.
|
|
260
|
+
Released versions are not rebuilt: `archive-docs-version` (in `publish-release`,
|
|
261
|
+
`X.Y.0` tags only) builds each one once and uploads it to its GitHub release, and
|
|
262
|
+
`deploy-docs` unpacks those archives under `/<version>/`. See `website/README.md`.
|
|
259
263
|
|
|
260
264
|
`terreno-gcp` uses CircleCI OIDC (`CIRCLE_OIDC_TOKEN_V2`), never a JSON service
|
|
261
265
|
account key. Set `circleci_org_id`, `circleci_project_id`, and
|
|
@@ -7,24 +7,23 @@ describes how they behaved if one is ever re-enabled.
|
|
|
7
7
|
|
|
8
8
|
## Docs site (`docs-deploy.yml`)
|
|
9
9
|
|
|
10
|
-
`Build docs site` is often the longest PR job because
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
`Build docs site` is often the longest PR job because TypeDoc regenerates API
|
|
11
|
+
pages. The site builds only the current docs tree; `website/versioned_docs/` is
|
|
12
|
+
markdown for the MCP server.
|
|
13
13
|
|
|
14
14
|
PR builds set `DOCS_PREVIEW=true` and pass `--no-minify`. Both PR and
|
|
15
15
|
`master` use Docusaurus Faster (Rspack/SWC via `@docusaurus/faster`).
|
|
16
16
|
|
|
17
17
|
| Behavior | PR preview | `master` production |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| Versioned docs (`57.1.0`, `0.30.0`, …) | Omitted
|
|
19
|
+
| Versioned docs (`57.1.0`, `0.30.0`, …) | Omitted | Prebuilt archives unpacked under `/<version>/` |
|
|
20
20
|
| JS minify | Off | On |
|
|
21
21
|
| Local search index | Skipped | Built |
|
|
22
22
|
| Generated API + component MDX | Restored from cache when `api`/`rtk`/`ui` hashes match | Same cache, then full generate on miss |
|
|
23
23
|
| TypeDoc workspace `tsc` | Once per generate (api+rtk deps share a process) | Same |
|
|
24
24
|
| Rspack cache (`node_modules/.cache/rspack`) | Restored per `pull_request` vs `push` | Separate production key |
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
PR deploy preview.
|
|
26
|
+
Do not rely on `/57.1.0/…` URLs in a PR deploy preview.
|
|
28
27
|
|
|
29
28
|
## Other high-cost jobs
|
|
30
29
|
|
|
@@ -152,7 +152,37 @@ import {useQuery} from "@terreno/syncdb/react";
|
|
|
152
152
|
const todos = useQuery<Todo>("todos", {filter: (t) => !t.completed});
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
`filter` / `sort` run in JS
|
|
155
|
+
`where` / `filter` / `sort` / `limit` run in JS over the local store. Tombstones are excluded unless `{includeDeleted: true}`.
|
|
156
|
+
|
|
157
|
+
### Filtered, sorted, and paged list args
|
|
158
|
+
|
|
159
|
+
RTK list args map to the same syntax. Pick the hook by collection size:
|
|
160
|
+
|
|
161
|
+
| RTK | Whole collection synced (`collections`) | Too large to sync whole (`queryCollections`) |
|
|
162
|
+
|-----|------------------------------------------|----------------------------------------------|
|
|
163
|
+
| `useGetMessagesQuery({threadId})` | `useQuery("messages", {where: {threadId}})` | `useWindowQuery("messages", {where: {threadId}})` |
|
|
164
|
+
| `{status: {$in: ["open", "pending"]}}` | `where: {status: {$in: [...]}}` | same |
|
|
165
|
+
| `{sort: "-created"}` | `sort: "-created"` | same |
|
|
166
|
+
| `{limit: 20}` | `limit: 20` | `pageSize: 20` |
|
|
167
|
+
| `{page: 2}` / load more | grow `limit` | `fetchNextPage()` |
|
|
168
|
+
| `data.more` / `data.total` | — | `hasMore` / `total` |
|
|
169
|
+
| `isLoading` / `isFetching` / `refetch` | not needed | `isLoading` / `isFetching` / `refetch` |
|
|
170
|
+
| `{skip: !threadId}` | not needed | `skip: !threadId` |
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import {useWindowQuery} from "@terreno/syncdb/react";
|
|
174
|
+
|
|
175
|
+
const {data: messages, hasMore, fetchNextPage, isLoading} = useWindowQuery<Message>("messages", {
|
|
176
|
+
where: {threadId},
|
|
177
|
+
sort: "-created",
|
|
178
|
+
pageSize: 30,
|
|
179
|
+
skip: !threadId,
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Windows with different filters keep separate result lists but share one local row per
|
|
184
|
+
document, so an edit to a message shown in two windows updates once. See
|
|
185
|
+
[Query windows](../reference/syncdb.md#query-windows).
|
|
156
186
|
|
|
157
187
|
### Delete this RTK read code
|
|
158
188
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Preview a component demo
|
|
2
2
|
|
|
3
|
-
Open a component demo and change the shareable query. The preview
|
|
4
|
-
dev component routes
|
|
3
|
+
Open a component demo and change the shareable query. The preview controls sit in the
|
|
4
|
+
navigation header on demo and dev component routes: dropdowns for theme, viewport, and
|
|
5
|
+
background, plus a settings button (sliders icon) that opens a modal for right-to-left,
|
|
6
|
+
reduced motion, locale, and the share query. In windows narrower than 768px every
|
|
7
|
+
preview control moves into that modal; the Dev/Demo mode link stays in the header. `?embed=1` applies the same query and hides
|
|
8
|
+
the controls.
|
|
5
9
|
|
|
6
10
|
```text
|
|
7
11
|
/demo/Button?theme=dark&viewport=375&background=inverse&locale=en-US&rtl=1&reducedMotion=1
|
|
@@ -16,4 +20,4 @@ dev component routes. `?embed=1` applies the same query and hides the bar.
|
|
|
16
20
|
| `rtl` | `1` sets document direction to `rtl` on web |
|
|
17
21
|
| `reducedMotion` | `1` sets `data-reduced-motion="1"` on the document element |
|
|
18
22
|
|
|
19
|
-
Reload keeps the query. The
|
|
23
|
+
Reload keeps the query. The settings modal shows the current query when any value is not the default. Choosing a control's default removes that key from the URL. Leaving the component route restores the app theme and, on web, document direction and reduced motion. Custom palettes stay on `/palette`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Run the harness on multiple instances
|
|
2
|
+
|
|
3
|
+
Run durable harness tasks on every instance of a scaled-out backend (for example several
|
|
4
|
+
Cloud Run instances). Swap the default `InProcessRunner`, where one process drains every
|
|
5
|
+
task, for `JobsRunner`, which runs each phase as a `@terreno/jobs` job. API:
|
|
6
|
+
[AI harness reference → JobsRunner](../reference/ai-harness.md#jobsrunner). Why it is safe:
|
|
7
|
+
[Durable agent harness → Runners](../explanation/durable-agent-harness.md#runners).
|
|
8
|
+
|
|
9
|
+
You need a working harness (see [Build a durable workflow](build-a-durable-workflow.md)) and
|
|
10
|
+
a MongoDB replica set. Task data does not change, so you can switch runners without a
|
|
11
|
+
migration.
|
|
12
|
+
|
|
13
|
+
## 1. Install the jobs package (1 minute)
|
|
14
|
+
|
|
15
|
+
`@terreno/jobs` is an optional peer of `@terreno/ai`. Add it to the app:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @terreno/jobs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 2. Open the harness with a JobsRunner (5 minutes)
|
|
22
|
+
|
|
23
|
+
Create the `JobsApp` first, hand it to the runner, and register it on the app. Build the
|
|
24
|
+
app before the harness starts: `harness.start()` throws when no `JobsApp` is registered.
|
|
25
|
+
The runner defines the `terreno.harness.phase` job on it.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import {Harness} from "@terreno/ai/harness";
|
|
29
|
+
import {JobsRunner} from "@terreno/ai/harness/jobsRunner";
|
|
30
|
+
import {TerrenoApp} from "@terreno/api";
|
|
31
|
+
import {JobsApp} from "@terreno/jobs";
|
|
32
|
+
|
|
33
|
+
const jobs = new JobsApp();
|
|
34
|
+
const harness = await Harness.open({
|
|
35
|
+
registry: [intakeSummary, summarizer],
|
|
36
|
+
runner: new JobsRunner({jobs}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const app = new TerrenoApp({userModel: User}).register(jobs);
|
|
40
|
+
// ...register HarnessApp and your routes, then build or start the server.
|
|
41
|
+
app.start();
|
|
42
|
+
|
|
43
|
+
await harness.start();
|
|
44
|
+
await jobs.startWorker();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Shut down in the reverse order so phases in flight finish:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
await harness.stop(); // stops dispatching, waits for this instance's phases
|
|
51
|
+
await jobs.stopWorker();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 3. Run the same code on every instance
|
|
55
|
+
|
|
56
|
+
Every instance runs the same startup. Each one:
|
|
57
|
+
|
|
58
|
+
- scans for runnable tasks and enqueues one job per runnable phase (duplicates collapse on
|
|
59
|
+
the job's idempotency key),
|
|
60
|
+
- runs phase jobs from the shared jobs queue, and
|
|
61
|
+
- sweeps task leases that expired because an instance died.
|
|
62
|
+
|
|
63
|
+
No instance is special, and none waits on standby.
|
|
64
|
+
|
|
65
|
+
## 4. Check it works (2 minutes)
|
|
66
|
+
|
|
67
|
+
1. Start two backend processes against the same database (different `PORT`s).
|
|
68
|
+
2. Create a task (for the example backend, run `startClinicalIntake`).
|
|
69
|
+
3. Open the admin **Jobs** screen. Each phase shows as its own `terreno.harness.phase` job
|
|
70
|
+
with payload `{taskId}`.
|
|
71
|
+
4. Kill one process mid-phase. Within one lease (`leaseDuration`, default 30 s) the other
|
|
72
|
+
process replays a `replay: "safe"` phase, or parks any other phase as `interrupted`.
|
|
73
|
+
|
|
74
|
+
## Tune it
|
|
75
|
+
|
|
76
|
+
| Want | Set |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| Faster pickup of new work | `new JobsRunner({jobs, pollInterval: {milliseconds: 200}})` and `new JobsApp({pollIntervalMs: 200})` |
|
|
79
|
+
| Faster crash recovery | A shorter `leaseDuration` with `heartbeatInterval` about a third of it |
|
|
80
|
+
| Phases longer than the jobs lock TTL | Nothing: the task lease, renewed by heartbeats, decides who commits |
|
|
81
|
+
|
|
82
|
+
## Troubleshoot
|
|
83
|
+
|
|
84
|
+
- **Jobs fail with `harness-runner-stopped`.** A jobs worker runs on an instance whose
|
|
85
|
+
harness is not started. Start the harness before `jobs.startWorker()`. The jobs worker
|
|
86
|
+
retries the job, so no task is lost.
|
|
87
|
+
- **`harness.start()` throws "JobsRunner needs a registered JobsApp".** The dispatcher
|
|
88
|
+
enqueues through the registered jobs service, and `TerrenoApp` registers plugins when it
|
|
89
|
+
builds. Call `app.start()` (or `build()`) before `harness.start()`.
|
|
90
|
+
- **A task stays `pending`.** Check that its `name@version` is in the registry of the
|
|
91
|
+
running instances. A runner only dispatches registered versions.
|
|
92
|
+
- **Pick one runner per database.** Mixing them is safe (every claim and commit is fenced),
|
|
93
|
+
but an `InProcessRunner` owner then claims tasks itself and runs them to a stop.
|
|
@@ -140,7 +140,7 @@ On resume, a task whose status is `running` with an expired lease was interrupte
|
|
|
140
140
|
| Runner | Ownership | Use |
|
|
141
141
|
|---|---|---|
|
|
142
142
|
| `InProcessRunner` (default) | One process holds the `HarnessOwner` lease (heartbeat). A second process gets `standby` and takes over on lease expiry. Polls runnable tasks; wakes on `runAt` and events. | Local agents, single-instance apps, tests |
|
|
143
|
-
| `JobsRunner` (`@terreno/ai/harness/jobsRunner`, optional peer `@terreno/jobs`) | Each runnable phase → job `terreno.harness.phase`, idempotency key `taskId:phase:attempt
|
|
143
|
+
| `JobsRunner` (`@terreno/ai/harness/jobsRunner`, optional peer `@terreno/jobs`) | Each runnable phase → job `terreno.harness.phase`, idempotency key `taskId:phase:attempt:claims` (`claims` counts claims, so a phase revisited by an event wake or crash replay gets a new job; jobs idempotency keys are permanent). Handler acquires the task lease (fenced, heartbeat) and runs one phase. The task lease, not the job lock, is the authority. | Multi-instance Cloud Run |
|
|
144
144
|
|
|
145
145
|
### Ownership tree
|
|
146
146
|
|