@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
|
@@ -10,6 +10,7 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
|
|
|
10
10
|
- [createSyncDb configuration](#createsyncdb-configuration)
|
|
11
11
|
- [SyncDb client methods](#syncdb-client-methods)
|
|
12
12
|
- [React hooks](#react-hooks)
|
|
13
|
+
- [Query windows](#query-windows)
|
|
13
14
|
- [Codegen](#codegen)
|
|
14
15
|
- [Conflict API](#conflict-api)
|
|
15
16
|
- [Sync status API](#sync-status-api)
|
|
@@ -25,7 +26,8 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
|
|
|
25
26
|
- `createSyncDb`, `SyncDb`, `SyncDbConfig`, `MutateArgs`
|
|
26
27
|
- `betterAuthAdapter`, `bridgeBetterAuthReactClient`, `AuthProvider`
|
|
27
28
|
- `listConflicts`, `wipeLocalData`, `generateMutationId`
|
|
28
|
-
-
|
|
29
|
+
- Queries: `QueryFieldNotAllowedError`, `ListRequestError`, `WhereFilter`, `SortSpec`, `WindowQuery`, `QueryWindowState`, `QueryWindows`, `compileWhere`, `DEFAULT_WINDOW_PAGE_SIZE`
|
|
30
|
+
- React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`
|
|
29
31
|
- CLI: `terreno-syncdb-codegen` (generates `SYNC_COLLECTIONS` + friendly hooks from OpenAPI)
|
|
30
32
|
- Testing (`@terreno/syncdb/testing`): `createFakeTransport`
|
|
31
33
|
|
|
@@ -165,6 +167,7 @@ be passed to `betterAuthAdapter` directly.
|
|
|
165
167
|
| `name` | `string` | — (required) | Persisted database name |
|
|
166
168
|
| `collections` | `string[]` | — (required) | Collection names to sync (local tables + subscriptions) |
|
|
167
169
|
| `windowCollections` | `string[]` | `[]` | Collections that join `{collection}\|admin`, skip snapshots/reconcile, and hydrate only known REST membership ids |
|
|
170
|
+
| `queryCollections` | `Array<string \| {collection, path?, fullSync?}>` | `[]` | Collections too large to sync whole (or `fullSync: true` to keep snapshot sync and add windows). No snapshot/reconcile paging; rows arrive through [query windows](#query-windows) against the `modelRouter` list endpoint (`path`, default `/{collection}`) plus live deltas on the user's normal streams. Each entry must also be in `collections` and not in `windowCollections`. |
|
|
168
171
|
| `organizationIdProvider` | `() => string \| undefined` | — | Read at send time: sets `X-Organization-Id` on HTTP sync calls and `organizationId` on socket mutate payloads |
|
|
169
172
|
| `authProvider` | `AuthProvider` | — (required) | `{getToken, getUserId, onAuthChange, refresh?}` |
|
|
170
173
|
| `baseUrl` | `string` | — | Server origin; required unless both `transport` and `httpChannel` are injected |
|
|
@@ -200,6 +203,7 @@ be passed to `betterAuthAdapter` directly.
|
|
|
200
203
|
| `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
|
|
201
204
|
| `reconcile()` | HTTP snapshot catch-up for every known stream; runs tombstone compaction on success. Also runs automatically on (re)connect, on a rate-limited seq-jump hint, and on the periodic timer; each `sync:subscribed` confirmation additionally pages just the streams it names. |
|
|
202
205
|
| `hydrateWindow({collection, ids, restRows?})` | Admin window upsert: REST rows (optional) land immediately; every requested id is also fetched from `GET /sync/entities` before this resolves so seq/deleted metadata is canonical for immediate update/delete. A `{collection}|admin` delta that lands while the fetch is in flight wins, so hydration never rewinds a row to an older seq. Unknown ids are ignored. |
|
|
206
|
+
| `queryWindows` | Server query windows for `queryCollections`: `fetchWindow({query, nextPage?})`, `getWindow(query)`, `retain(query)` → release, `refetchRetained()`, `pruneUnretained()`, `subscribe(cb)`, `keyFor(query)`. See [Query windows](#query-windows). |
|
|
203
207
|
| `forceResync()` | Purge every known stream locally and re-bootstrap from cursor 0 (outbox/conflicts untouched). Returns `{ok, reason?, streams, purged, repaired}`. After discovery and each stream bootstrap it waits for in-flight `start`/`stop`/auth-change work, then abandons with `reason: "superseded"` when that work switched users or generations. |
|
|
204
208
|
| `replayOutbox()` | Drain queued mutations for the current user now. |
|
|
205
209
|
| `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
|
|
@@ -271,13 +275,26 @@ Subscribe to a single entity. Re-renders when that row changes. `isPending` is t
|
|
|
271
275
|
|
|
272
276
|
```typescript
|
|
273
277
|
const todos = useQuery<Todo>("todos", {
|
|
274
|
-
|
|
275
|
-
sort:
|
|
276
|
-
|
|
278
|
+
where: {completed: false, created: {$gte: since}},
|
|
279
|
+
sort: "-created",
|
|
280
|
+
limit: 20,
|
|
277
281
|
});
|
|
282
|
+
|
|
283
|
+
// Callbacks still work and combine with `where`:
|
|
284
|
+
const mine = useQuery<Todo>("todos", {filter: (t) => t.ownerId === userId, sort: byCreatedDesc});
|
|
278
285
|
```
|
|
279
286
|
|
|
280
|
-
Returns decoded entity data arrays. Tombstones excluded unless `includeDeleted: true`.
|
|
287
|
+
Returns decoded entity data arrays from the local store. Tombstones excluded unless `includeDeleted: true`.
|
|
288
|
+
|
|
289
|
+
| Option | Type | Description |
|
|
290
|
+
|--------|------|-------------|
|
|
291
|
+
| `where` | `WhereFilter` | Mongo-style filter: equality, dot paths, `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin` `$exists`, `$and` / `$or`. An array field matches when any element equals the value. Compared by value, so inline objects are fine. Throws for operators only the server can run (`$search`, `$regex`). |
|
|
292
|
+
| `filter` | `(data) => boolean` | Arbitrary predicate, applied after `where`. |
|
|
293
|
+
| `sort` | `SortSpec \| comparator` | `"-created name"`, `{created: "descending"}`, or `(a, b) => number`. |
|
|
294
|
+
| `limit` | `number` | Keep the first N rows after filter and sort. |
|
|
295
|
+
| `includeDeleted` | `boolean` | Include tombstones. |
|
|
296
|
+
|
|
297
|
+
Filter and sort run in JS — memoize `filter`/comparator callbacks when collections are large.
|
|
281
298
|
|
|
282
299
|
### `useEntityIds(collection, options?)`
|
|
283
300
|
|
|
@@ -288,6 +305,38 @@ return ids.map((id) => <TodoRow key={id} id={id} />);
|
|
|
288
305
|
|
|
289
306
|
Same options as `useQuery`, but returns only ordered ids with **referential stability** — the array identity changes only when membership or order changes, not on field updates. Pair with per-row `useEntity` for large lists.
|
|
290
307
|
|
|
308
|
+
### `useWindowQuery(collection, options?)`
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const {data, isLoading, isFetching, hasMore, fetchNextPage, refetch, total, error} =
|
|
312
|
+
useWindowQuery<Message>("messages", {
|
|
313
|
+
where: {threadId, deleted: false},
|
|
314
|
+
sort: "-created",
|
|
315
|
+
pageSize: 30,
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Server-filtered list over a `queryCollections` collection. See [Query windows](#query-windows).
|
|
320
|
+
|
|
321
|
+
| Option | Type | Default | Description |
|
|
322
|
+
|--------|------|---------|-------------|
|
|
323
|
+
| `where` | `WhereFilter` | — | Sent as qs list params (`status[$in][0]=open`). Fields must be in the route's `queryFields`. |
|
|
324
|
+
| `sort` | `SortSpec` | route default | Server `sort`; also orders the local view. |
|
|
325
|
+
| `pageSize` | `number` | `50` | Server `limit` per page (capped by the route's `maxLimit`). |
|
|
326
|
+
| `skip` | `boolean` | `false` | Do not retain or fetch (e.g. a required param is missing). |
|
|
327
|
+
| `liveMatch` | `boolean` | `true` | Show cached rows that newly match `where` inside the loaded range. |
|
|
328
|
+
|
|
329
|
+
| Result | Description |
|
|
330
|
+
|--------|-------------|
|
|
331
|
+
| `data` / `ids` | This window's rows in view order. |
|
|
332
|
+
| `isLoading` | No membership yet (first fetch in flight). Cached membership renders with `isLoading: false`. |
|
|
333
|
+
| `isFetching` | Any request for this window is in flight. |
|
|
334
|
+
| `isError` / `error` | Last request failed (cached rows still render). |
|
|
335
|
+
| `errorCode` | Server error code of the last failure; `"query-param-not-allowed"` means a `where` field is missing from `queryFields`. |
|
|
336
|
+
| `hasMore` / `total` | From the list response's `more` / `total`. |
|
|
337
|
+
| `fetchNextPage()` | Append the next server page. |
|
|
338
|
+
| `refetch()` | Reload every loaded page in one request, replacing membership. |
|
|
339
|
+
|
|
291
340
|
### `useMutate(collection)`
|
|
292
341
|
|
|
293
342
|
```typescript
|
|
@@ -331,7 +380,7 @@ Returns the `SyncDb` instance from context (escape hatch for imperative calls li
|
|
|
331
380
|
|
|
332
381
|
### `createCollectionHooks`
|
|
333
382
|
|
|
334
|
-
Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns
|
|
383
|
+
Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns six operation hooks (`useListQuery`, `useWindowQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`). Generated SDKs rename them to friendly names (`useTodos`, `useTodosWindow`, `useTodo`, `useCreateTodo`, …). `useWindowQuery` works only for collections listed in `queryCollections`. Mutation hooks return `[trigger]`; triggers apply locally and return `{mutationId, id}` synchronously. Optional `retries` maps to `maxAttempts` (`false` → 1, a number → that many, omitted → engine default).
|
|
335
384
|
|
|
336
385
|
```typescript
|
|
337
386
|
export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
|
|
@@ -340,6 +389,107 @@ export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
|
|
|
340
389
|
});
|
|
341
390
|
```
|
|
342
391
|
|
|
392
|
+
## Query windows
|
|
393
|
+
|
|
394
|
+
Use query windows when a collection is too large to sync whole (messages, events, audit
|
|
395
|
+
rows). They match what an RTK list query did — `where`, `sort`, `limit`, `page`, `more`,
|
|
396
|
+
`total` — but keep the data local-first and deduplicated.
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
export const syncDb = createSyncDb({
|
|
400
|
+
collections: ["todos", "messages"],
|
|
401
|
+
queryCollections: [{collection: "messages", path: "/messages"}],
|
|
402
|
+
// ...
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Membership, not copies.** A window is one list query against the collection's
|
|
407
|
+
`modelRouter` list endpoint. Its result is an ordered id list stored in the reserved
|
|
408
|
+
`_queryWindows` table. Each row lands once in the collection's entity table, keyed by id.
|
|
409
|
+
Two windows that overlap share those rows:
|
|
410
|
+
|
|
411
|
+
- A delta, mutation, or refetch for a shared row writes the store once; every window that
|
|
412
|
+
lists the row re-renders from it.
|
|
413
|
+
- Each window lists only its own members. Window B never shows rows that only window A
|
|
414
|
+
fetched.
|
|
415
|
+
|
|
416
|
+
**Live membership.** When `where` is locally evaluable (see `useQuery`'s `where`), a window
|
|
417
|
+
also:
|
|
418
|
+
|
|
419
|
+
- hides a member whose fields stop matching (an unpinned message leaves the "pinned" window);
|
|
420
|
+
- shows cached rows that newly match (a live delta, or a local create) when they sort inside
|
|
421
|
+
the loaded range. With more pages unloaded, a row that sorts after the last loaded row
|
|
422
|
+
waits for `fetchNextPage()`. Without a `sort` and with more pages, only local creates are
|
|
423
|
+
admitted.
|
|
424
|
+
|
|
425
|
+
A full fetch also records cached rows that matched `where` locally but that the server did
|
|
426
|
+
not return (deleted, changed, or no longer visible while this device was offline). They stay
|
|
427
|
+
out of the view until a newer delta or a local edit changes them, so stale cached data never
|
|
428
|
+
resurrects a row.
|
|
429
|
+
|
|
430
|
+
Set `liveMatch: false` to show server membership only. Operators only the server runs
|
|
431
|
+
(`$search`) also fall back to server membership.
|
|
432
|
+
|
|
433
|
+
**Fetching.** The first `retain` of a window in a session fetches it; persisted membership
|
|
434
|
+
renders meanwhile, so windows work offline. Concurrent fetches of the same window share one
|
|
435
|
+
request; a refetch and a `fetchNextPage()` on one window run one after the other. Retained
|
|
436
|
+
windows refetch on reconnect and after `forceResync()` — query collections
|
|
437
|
+
skip snapshot catch-up, so this is how deltas missed offline are recovered. Windows not on
|
|
438
|
+
screen at that moment refetch the next time they are shown. Window fetches
|
|
439
|
+
started before a `stop()` or user switch are discarded.
|
|
440
|
+
|
|
441
|
+
**Seq.** List rows that carry `_syncSeq` (the `syncPlugin` field) are written with that seq.
|
|
442
|
+
Rows without it (a `responseHandler` that strips it) are re-read from `GET /sync/entities` so
|
|
443
|
+
a later mutation never sends a stale `baseVersion`. Rows with a pending outbox mutation, or
|
|
444
|
+
with a newer seq from a delta, are never overwritten. A list `responseHandler` must return the
|
|
445
|
+
same shape as the sync serializer; otherwise list rows and delta rows differ.
|
|
446
|
+
|
|
447
|
+
**Missing `queryFields`.** Every `where` field must be in the route's `queryFields`. The
|
|
448
|
+
server answers 400 `query-param-not-allowed` naming the field, the model, and the allowed
|
|
449
|
+
list. syncdb turns it into `QueryFieldNotAllowedError` (`field`, `allowedQueryFields`,
|
|
450
|
+
`model`), logs it with `console.error`, sets the window's `errorCode` to
|
|
451
|
+
`"query-param-not-allowed"`, and rejects `fetchWindow` / `refetch` / `fetchNextPage` with it.
|
|
452
|
+
Other list failures throw `ListRequestError` (`status`, `code`, `detail`).
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import {QueryFieldNotAllowedError} from "@terreno/syncdb";
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
await refetch();
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof QueryFieldNotAllowedError) {
|
|
461
|
+
// error.field === "title", error.allowedQueryFields === ["completed", "created", "ownerId"]
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**`maxLimit` and pipelining.** `modelRouter` clamps `limit` to the route's `maxLimit` and
|
|
467
|
+
echoes the applied value. Window requests always send `page` (so the server never logs a
|
|
468
|
+
truncated unpaginated list). When a fetch needs more rows than one request returns — a
|
|
469
|
+
`refetch()` after several `fetchNextPage()` calls, or a `pageSize` above `maxLimit` — syncdb
|
|
470
|
+
learns the cap from the echoed `limit` and splits the range into sequential page requests
|
|
471
|
+
of `maxLimit` rows starting at the aligned page that contains the range, trimming the extra
|
|
472
|
+
rows, so offsets stay exact. Each pipelined fetch logs a
|
|
473
|
+
`console.warn` naming the row count, the cap, and the request count. Fix it by loading fewer
|
|
474
|
+
pages, lowering `pageSize`, or raising `maxLimit` on the route.
|
|
475
|
+
|
|
476
|
+
**Fully synced collections.** `queryCollections: [{collection: "todos", fullSync: true}]`
|
|
477
|
+
keeps normal snapshot sync and also enables windows, for server-side filtering, paging, and
|
|
478
|
+
totals over a collection that is already local. See `example-frontend/app/todo-windows.tsx`.
|
|
479
|
+
|
|
480
|
+
**Limits.**
|
|
481
|
+
|
|
482
|
+
- `refetch()` with several pages loaded reloads `pageSize × pages` rows (see below).
|
|
483
|
+
- Page appends dedupe ids. A delete between page loads can shift offsets and skip one row
|
|
484
|
+
until the next refetch.
|
|
485
|
+
- `null` in `where` is rejected (qs would send the string `"null"`); use `{$exists: false}`.
|
|
486
|
+
A top-level `{$in: []}` returns an empty window without a request; an empty array anywhere
|
|
487
|
+
else throws instead of silently widening the query.
|
|
488
|
+
- Rows stay in the entity table after their windows unmount. `start()` prunes persisted
|
|
489
|
+
membership of unretained windows not fetched for 7 days (`pruneStale`);
|
|
490
|
+
`pruneUnretained()` drops all unretained membership. Rows are dropped by leave-purge,
|
|
491
|
+
tombstone compaction, or a wipe.
|
|
492
|
+
|
|
343
493
|
## Codegen
|
|
344
494
|
|
|
345
495
|
`terreno-syncdb-codegen` is a bin of `@terreno/syncdb`. It reads OpenAPI, discovers list operations with `x-terreno-sync`, and writes typed hooks plus `SYNC_COLLECTIONS`.
|
|
@@ -725,11 +725,20 @@ Master-detail layout. Pass `listViewData` plus `renderListViewItem` for the list
|
|
|
725
725
|
screens the detail replaces the list until the user goes back.
|
|
726
726
|
|
|
727
727
|
`desktopChildrenMinWidth` opts into a minimum pixel width for each desktop child when there
|
|
728
|
-
are one or two children. `
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
728
|
+
are one or two children. `childColumnRounding` sets the border radius on each desktop detail
|
|
729
|
+
pane (defaults to `md`, 4px); the list column is excluded. The pane clips to that radius. The
|
|
730
|
+
column inside it scrolls: content taller than the pane scrolls in the column, and a child with
|
|
731
|
+
`height: "100%"` fills the visible pane so a scroll view inside that child scrolls on its own.
|
|
732
|
+
On a narrow web viewport the pager page is height-bounded to that same visible pane. Its rounded
|
|
733
|
+
corners still clip, and a `height: "100%"` child scrolls inside the page. On the labeled pager,
|
|
734
|
+
previous and next sit in fixed left and right slots in a row under the column. A missing direction
|
|
735
|
+
leaves that slot empty, so the other arrow stays put. The column height is the measured pane minus
|
|
736
|
+
top spacing, the navigation row, bottom spacing, and `bottomNavBarHeight`.
|
|
737
|
+
`narrowViewportChildLabels`
|
|
738
|
+
opts into a labeled full-width pager on the narrow viewport. `narrowBelowWidth` uses that
|
|
739
|
+
viewport when the window is at or below the given pixel width; when omitted, the narrow
|
|
740
|
+
viewport follows `isNarrowViewport()`. These props are web only; the native `SplitPage`
|
|
741
|
+
ignores them. See `SplitPageProps` for when each prop applies and what is ignored.
|
|
733
742
|
`IconButton`'s `backgroundOpacity` tints only that button's background.
|
|
734
743
|
|
|
735
744
|
```typescript
|
|
@@ -131,21 +131,23 @@ created in 1.1 and extended by every later task — never deferred.
|
|
|
131
131
|
|
|
132
132
|
## Phase 2 — Multi-instance
|
|
133
133
|
|
|
134
|
-
- [
|
|
134
|
+
- [x] **Task 2.0**: `InProcessRunner` concurrency
|
|
135
135
|
- Delivers: `new InProcessRunner({concurrency})` (default 8) runs up to N claimed tasks at once, each with its own lease and heartbeat; a slow phase no longer delays other tasks' wakes; `stop()` waits for all in-flight phases
|
|
136
136
|
- Files: `ai/src/harness/runners/inProcessRunner.ts`, tests
|
|
137
137
|
- Blocked by: 1.12
|
|
138
138
|
- Skills: `terreno-backend-api`
|
|
139
139
|
- Docs: reference "Runners" (concurrency option, ordering guarantees)
|
|
140
140
|
- Acceptance: bun test — a phase blocked on a gate does not prevent a second task from completing; never more than N concurrent; no task runs twice concurrently
|
|
141
|
+
- Shipped: fake-context cases in `ai/src/harness/runners/inProcessRunner.test.ts` (default 8, gate, peak, stop waits for all, invalid concurrency) and real-Mongo cases in `ai/src/harness/concurrency.test.ts` (blocked task does not hold up a second; 7 two-phase tasks at concurrency 3 peak at exactly 3 with no task running twice). Roast: PASS (2026-10-04; 76 targeted tests, three mutation checks red)
|
|
141
142
|
|
|
142
|
-
- [
|
|
143
|
+
- [x] **Task 2.1**: `JobsRunner`
|
|
143
144
|
- Delivers: `@terreno/ai/harness/jobsRunner` with optional peer `@terreno/jobs`; job `terreno.harness.phase` keyed `taskId:phase:attempt`; harness task lease + heartbeat is the authority; importing the root `@terreno/ai` never loads jobs
|
|
144
145
|
- Files: `ai/src/harness/runners/jobsRunner.ts`, `ai/package.json`, tests
|
|
145
146
|
- Blocked by: 2.0
|
|
146
147
|
- Skills: `terreno-backend-api`
|
|
147
148
|
- Docs: reference "Runners"; how-to "Run on multiple instances"
|
|
148
149
|
- Acceptance: AC11 — two workers race one phase; exactly one commit; stale owner rejected; knip clean without jobs installed
|
|
150
|
+
- Shipped: `ai/src/harness/runners/jobsRunner.test.ts` (two-worker race, stale-lease commit rejected, re-dispatch of a dead job, root imports never reach `@terreno/jobs`). Drift: the job key is `taskId:phase:attempt:claims` — `claims` (new `HarnessTask` counter) gives a phase revisited by an event wake or crash replay its own job, since jobs idempotency keys are permanent. Built before Task 2.0; it does not depend on `InProcessRunner` concurrency. Roast: PASS (2026-10-04; 17 targeted tests, ai suite 1185/0, knip clean, two mutation checks red)
|
|
149
151
|
|
|
150
152
|
- [ ] **Task 2.2**: Runs admin screen
|
|
151
153
|
- Delivers: task list/tree with status filters, abort, resolve-interrupted (retry/abort/complete + reason), link to trace
|