@tpsdev-ai/flair 0.51.2 → 0.53.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.
Files changed (74) hide show
  1. package/README.md +10 -5
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1037 -566
  4. package/dist/doctor-client.js +35 -0
  5. package/dist/hook-install.js +74 -0
  6. package/dist/install/global-bin-path.js +14 -0
  7. package/dist/lib/auth-resolve.js +15 -0
  8. package/dist/lib/doctor-run.js +28 -15
  9. package/dist/lib/launchd-repair.js +198 -0
  10. package/dist/lib/stabilize-mqtt-network.js +123 -0
  11. package/dist/lib/upgrade-exec-path.js +257 -0
  12. package/dist/lib/upgrade-plain-tree.js +558 -0
  13. package/dist/rem/promote-policy.js +204 -0
  14. package/dist/rem/restore.js +55 -15
  15. package/dist/rem/runner.js +203 -20
  16. package/dist/resources/AdminMemory.js +2 -1
  17. package/dist/resources/AgentSeed.js +26 -10
  18. package/dist/resources/Asset.js +203 -0
  19. package/dist/resources/AutoPromoteCandidates.js +2 -4
  20. package/dist/resources/Credential.js +14 -0
  21. package/dist/resources/Federation.js +80 -0
  22. package/dist/resources/Integration.js +12 -0
  23. package/dist/resources/Memory.js +158 -60
  24. package/dist/resources/MemoryBootstrap.js +63 -20
  25. package/dist/resources/MemoryCandidate.js +12 -0
  26. package/dist/resources/MemoryConsolidate.js +2 -1
  27. package/dist/resources/MemoryDedupStats.js +17 -2
  28. package/dist/resources/MemoryFeed.js +30 -0
  29. package/dist/resources/MemoryGrant.js +14 -0
  30. package/dist/resources/MemoryReflect.js +75 -17
  31. package/dist/resources/Message.js +190 -0
  32. package/dist/resources/OrgEvent.js +12 -0
  33. package/dist/resources/PromoteMemoryCandidate.js +76 -0
  34. package/dist/resources/RecordUsage.js +1 -1
  35. package/dist/resources/Relationship.js +12 -0
  36. package/dist/resources/SemanticSearch.js +45 -13
  37. package/dist/resources/Soul.js +54 -18
  38. package/dist/resources/WorkspaceState.js +12 -0
  39. package/dist/resources/auth-middleware.js +17 -44
  40. package/dist/resources/authority-field-guard.js +37 -0
  41. package/dist/resources/bm25-index-service.js +1 -1
  42. package/dist/resources/bm25-index.js +50 -11
  43. package/dist/resources/embedding-space-guard.js +238 -0
  44. package/dist/resources/embeddings-provider.js +32 -5
  45. package/dist/resources/federation-classify.js +23 -1
  46. package/dist/resources/health.js +11 -2
  47. package/dist/resources/hit-tracking.js +244 -0
  48. package/dist/resources/mcp-tools.js +272 -7
  49. package/dist/resources/memory-reflect-lib.js +111 -0
  50. package/dist/resources/migrations/embedding-stamp.js +22 -4
  51. package/dist/resources/owner-field-guard.js +62 -0
  52. package/dist/resources/promotion-stamp.js +29 -0
  53. package/dist/resources/record-owner-guard.js +71 -5
  54. package/dist/resources/record-types.js +30 -7
  55. package/dist/resources/relay-lib.js +205 -0
  56. package/dist/resources/relay-ops.js +294 -0
  57. package/dist/resources/skill-write.js +120 -0
  58. package/dist/resources/soul-adk-guard.js +68 -0
  59. package/dist/resources/soul-write-policy.js +63 -0
  60. package/dist/resources/table-helpers.js +2 -0
  61. package/dist/resources/usage-recording.js +3 -3
  62. package/dist/src/rem/promote-policy.js +204 -0
  63. package/docs/api-reference.md +374 -0
  64. package/docs/auth.md +52 -0
  65. package/docs/federation.md +4 -0
  66. package/docs/integrations.md +6 -6
  67. package/docs/mcp-clients.md +16 -1
  68. package/docs/releasing.md +11 -8
  69. package/docs/rem.md +20 -2
  70. package/docs/upgrade.md +47 -2
  71. package/package.json +6 -5
  72. package/schemas/memory.graphql +51 -2
  73. package/schemas/message.graphql +74 -0
  74. package/templates/launchd/start-flair-with-admin-pass.sh +73 -0
@@ -90,13 +90,13 @@ n8n still uses Harper admin Basic auth — it is not this path. See [n8n.md](n8n
90
90
 
91
91
  No install step needed — every snippet below uses `npx -y @tpsdev-ai/flair-mcp`, which fetches and runs the server on demand (zero-install). The fastest path is `flair init`, which detects and wires these clients for you. To wire by hand, drop the relevant snippet into each tool's MCP config:
92
92
 
93
- **Claude Code** (`~/.config/claude-code/config.toml` or per-project `.claude/config.toml`):
94
- ```toml
95
- [mcp.servers.flair]
96
- command = "npx"
97
- args = ["-y", "@tpsdev-ai/flair-mcp"]
98
- env = { FLAIR_AGENT_ID = "claude-code" }
93
+ **Claude Code** (user scope lives in `~/.claude.json` the file `claude mcp add`, `flair init`, and `flair doctor` all read and write):
94
+ ```bash
95
+ claude mcp add flair --scope user \
96
+ -e FLAIR_AGENT_ID=claude-code \
97
+ -- npx -y @tpsdev-ai/flair-mcp
99
98
  ```
99
+ Or, for a project-scoped server checked into the repo, add the same `mcpServers.flair` block (Cursor's shape below) to `.mcp.json`.
100
100
 
101
101
  **Cursor** (`~/.cursor/mcp.json`):
102
102
  ```json
@@ -284,7 +284,7 @@ Twelve tools, kept deliberately small:
284
284
  | `memory_delete` | Remove a memory |
285
285
  | `relationship_store` | Record a subject-predicate-object relationship triple (e.g. "nathan manages flair") |
286
286
  | `bootstrap` | Get session-start context: soul + recent memories + predicted-relevant context |
287
- | `soul_set` | Set a personality/project/standards entry included in every bootstrap |
287
+ | `soul_set` | Runtime writes are refused; use operator credentials through `flair soul set` |
288
288
  | `soul_get` | Get a soul entry |
289
289
  | `flair_workspace_set` | Set your agent's current workspace state (ref/branch, phase, task) in the Office Space |
290
290
  | `flair_orgevent` | Publish an org-wide coordination event (claim/release/status) to the Office Space |
@@ -355,3 +355,18 @@ Future MCP-capable agent CLIs (and there are more landing every month) will work
355
355
  **Tool calls succeed but the agent doesn't see results in subsequent turns.** Check that the CLI is actually invoking `bootstrap` at session start — most CLIs need an explicit prompt nudge ("call the bootstrap tool now") on first use. Subsequent turns should pick up automatically once the CLI sees the schema.
356
356
 
357
357
  For deeper issues see [`troubleshooting.md`](troubleshooting.md) and the [`@tpsdev-ai/flair-mcp` repo](https://github.com/tpsdev-ai/flair/tree/main/packages/flair-mcp).
358
+
359
+
360
+ ### Task-aware bootstrap budgets
361
+
362
+ Pass `currentTask` when the session's task is known. Bootstrap reserves 30% of
363
+ `maxTokens` for task-relevant memories after Soul admission, protecting recall
364
+ from large pinned sets. Task retrieval can also spend any other remaining space;
365
+ unused space returns to pinned memories. This changes admission priority, not
366
+ retrieval ranking or visibility. Individual records still must fit, including
367
+ trust metadata when requested. A record larger than the available allowance
368
+ can still be omitted.
369
+
370
+ Without a task (including blank text), bootstrap retains its existing admission
371
+ policy. `taskRetrievalHint`, when present, explains unavailable embeddings, an
372
+ empty candidate pool, exhausted budget, or candidates already included/too large.
package/docs/releasing.md CHANGED
@@ -87,14 +87,17 @@ tag you pushed is the trigger). Watch the run; when it's green, the packages are
87
87
  but **not yet live**.
88
88
 
89
89
  In parallel — and **independent of the npm staging approval** — a `github-release` job
90
- auto-cuts a [GitHub release](https://github.com/tpsdev-ai/flair/releases) for the tag,
91
- using the matching `## [X.Y.Z]` section of `CHANGELOG.md` as the release notes (extracted
92
- by `scripts/changelog-extract.mjs`). It is idempotent: re-running the workflow or
93
- re-pushing the tag updates the existing release rather than failing. The GitHub release
94
- documents the tagged commit immediately; it does not wait on the npm 2FA gate. If the
95
- CHANGELOG has no section for the version, this job fails loudly rather than cutting an
96
- empty release which is why phase 1's fragment assembly refuses to produce an empty
97
- section rather than letting the failure surface here, after the tag is already pushed.
90
+ auto-cuts a [GitHub release](https://github.com/tpsdev-ai/flair/releases) for the tag.
91
+ Release notes are a **lede + links** rendering of the matching `## [X.Y.Z]` section
92
+ (`scripts/changelog-release-notes.mjs`): each entry keeps its bold lede, up to three
93
+ issue links, and any `> **Heads-up:**` operator lines. The deep record stays in
94
+ `CHANGELOG.md` and is linked from the footer at that tag. It is idempotent: re-running
95
+ the workflow or re-pushing the tag updates the existing release rather than failing.
96
+ The GitHub release documents the tagged commit immediately; it does not wait on the
97
+ npm 2FA gate. If the CHANGELOG has no section for the version, this job fails loudly
98
+ rather than cutting an empty release — which is why phase 1's fragment assembly
99
+ refuses to produce an empty section rather than letting the failure surface here,
100
+ after the tag is already pushed.
98
101
 
99
102
  > `workflow_dispatch` with a `version` input remains as a manual fallback (needs
100
103
  > `Actions: write`), but the tag push is the normal path.
package/docs/rem.md CHANGED
@@ -52,28 +52,46 @@ Selects which `models.generative.<logicalName>` entry a REM call uses. Unset →
52
52
 
53
53
  `flair rem nightly enable` installs a platform-native timer (launchd / systemd) **on the host it runs on**. In a multi-node or Fabric deploy, enabling it on every node would run the cycle N times and scatter N sets of pre-cycle snapshots. The v1 rule: **exactly one node gets the timer** — pick it deliberately, the same way you'd pick a cron owner for any single-writer job. This is a v1 constraint, not a permanent one; see #709 for the roadmap toward a coordinated multi-node story.
54
54
 
55
- Snapshot locality follows from this: a nightly cycle's pre-run snapshot (`~/.flair/snapshots/<agent>/`) lands on **the node that ran that cycle** — `flair rem restore <date>` and `flair rem snapshot list` only see local snapshots. If you move which node owns the timer, snapshot history doesn't move with it.
55
+ Snapshot locality follows from this: a nightly cycle's pre-run snapshot (`~/.flair/snapshots/<agent>/`) lands on **the node that ran that cycle** — `flair rem restore <date>` and `flair rem snapshot list` only see local snapshots. If you move which node owns the timer, snapshot history doesn't move with it. Live replay (`flair rem restore <date> --apply`) rewrites Soul with operator credentials (`--admin-pass` / `--admin-pass-file`); an agent key is refused. Leftover MemoryCandidate rows for that agent are deleted first so their claim text cannot block Soul restore.
56
56
 
57
57
  ## Interactive vs nightly
58
58
 
59
59
  - **Interactive (`flair rem rapid`):** one bounded, synchronous distillation call — gather cap 50 memories, bounded output tokens, seconds not minutes. Executes by default, staging candidates and printing a summary; `--prompt-only` returns the reflection prompt instead, for the bring-your-own-model handoff.
60
60
  - **Nightly (`flair rem nightly enable` / `run-once`):** fully detached — the scheduler runs the full cycle (snapshot → maintenance → distillation), candidates land as pending rows, and an audit row lands in `~/.flair/logs/rem-nightly.jsonl`. The operator reviews in the morning via `flair rem candidates`.
61
61
 
62
+ ### Safety bounds (a large backlog must not take the instance down)
63
+
64
+ A first run over thousands of unreflected memories used to hold the Harper main thread long enough that `/Health` and search timed out. Nightly distillation is now bounded:
65
+
66
+ - **Refuse to start** when `GET /Health` cannot be served within 2 seconds. The cycle logs `status: "refused"` and prints the reason; the scheduler will try again next night. Restore `/Health` before retrying, or `flair rem pause` to stop the timer.
67
+ - **Per-run cap** — at most 50 memories per `/ReflectMemories` call (`FLAIR_REM_MAX_MEMORIES`, hard ceiling 200), oldest-unreflected first so a 3k backlog drains across nights instead of one blocking run. Already-reflected rows fill leftover slots only when fewer than N unreflected matches remain. Nightly uses `scope: "all"` for ordinary (non-ADK) agents so the cap sees the whole backlog, not only the last 24 hours.
68
+ - **Yield + abort** — the gather scan yields so `/Health` and reads keep serving. `flair rem pause` (or `flair rem abort`) writes `~/.flair/rem.paused`; an in-flight gather on the same host stops at the next yield without restarting Harper.
69
+
70
+ `flair rem rapid` uses the same gather cap, oldest-unreflected selection, yield, and abort checks. It still defaults to `scope: "recent"` (last 24 hours) unless you pass `--since` / a wider scope.
71
+
62
72
  Either path, the review loop is the same: `flair rem candidates` lists pending rows, `flair rem promote <id> --rationale "<why>"` / `flair rem reject <id> --reason "<why>"` decide them. Nothing self-promotes except the narrow ADK per-user path ([Auto-promote](#auto-promote-adk-only)) — see [`docs/notes/rem-ux.md`](notes/rem-ux.md) for why that gate is load-bearing and how the surface is expected to evolve.
63
73
 
64
74
  ### ADK agents — per-user (per-tag) distillation
65
75
 
66
76
  adk-flair collapses every `(app, user)` into **one** Flair agentId, separating users only by a per-user tag `adk:<app>:<user>`. Distilling such an agentId with the default `scope:"recent"` would mix every user's sessions into shared claims — cross-user bleed. The nightly cycle therefore detects the agent's active `adk:<app>:<user>` tags (from the memories it already loads for the snapshot, with a recency cutoff that skips idle users and is scoped to the agent's own records) and runs distillation **once per tag** under `scope:"tagged"`, so each user's candidates come only from that user's own sessions. Agents with no `adk:` tags distill agentId-wide exactly as before.
67
77
 
78
+ Memory promotion runs through `POST /PromoteMemoryCandidate` with `candidateId` and a nonempty `rationale`. The server reads the candidate and its lineage, writes the persistent memory through the normal safety/provenance path, and stamps `promotionStatus`, `promotedAt` and `promotedBy`. Direct Memory writes cannot create, change or clear these workflow fields, including administrator REST writes. Full-row updates may echo unchanged stamps; omitted stamps are preserved. Creating a successor with `memory_update(preserveHistory=true)` leaves the verdict on the predecessor; the new content starts without an approval stamp. Non-admin reviewers are attributed to their authenticated identity; only administrators may name another reviewer. `flair rem promote --to memory` uses this endpoint and requires an updated server; it does not fall back to direct verdict writes.
79
+
68
80
  A candidate distilled under a tag records that tag in its `scopeTag` field. `flair rem promote` reads `scopeTag` as the authoritative per-user lineage tag and propagates it onto the promoted memory — so the promoted claim stays in that user's retrieval scope even if the source episodes are later archived or deleted. The single-node timer rule above is unchanged; the per-tag loop runs inside the one cycle on the one node. The non-thinking-model requirement (above) still holds — the per-tag path calls the same `models.generate()` route.
69
81
 
70
82
  #### Auto-promote (ADK only)
71
83
 
72
84
  For ADK agents, the nightly cycle **auto-promotes** these `scopeTag`-bearing candidates to the user's own persistent memory immediately after distillation — the one place REM does not wait for a human `rem promote`. The safety argument is blast-radius, not identity: the claim is distilled from a user's own sessions into that same user's own tag scope, so no cross-agent or Soul trust boundary is crossed. The promotion is enforced entirely server-side (`POST /AutoPromoteCandidates`), never by a CLI flag a compromised agent key could flip, and holds four invariants:
73
85
 
74
- - **Memory only, never Soul.** The target is hard-locked to `memory`; there is no Soul code path (Soul is agentId-scoped and cannot carry a per-user tag, so an ADK-sourced Soul promotion would be cross-user by construction).
86
+ - **Memory only, never Soul.** The target is hard-locked to `memory`; there is no Soul code path (Soul is agentId-scoped and cannot carry a per-user tag, so an ADK-sourced Soul promotion would be cross-user by construction). Soul mutations require operator credentials and refuse values matching any stored Memory or MemoryCandidate. This also prevents scripted promotion of learned claims, regardless of connector. See [Soul authorship](auth.md#soul-authorship).
75
87
  - **Fail-closed tag lineage.** A candidate is promoted only if it carries an authoritative `adk:<app>:<user>` scope tag, which the promoted memory then carries. The promoted memory is written `visibility:"private"` (owner-only) — not the org-open `shared` default a `persistent` write would otherwise get — so it is reachable only through the app agent's own tag-filtered search (which re-verifies the tag), invisible both to another user's tag filter and to every other agent on the instance. A candidate whose scope tag is absent or blank is left pending, never promoted tagless into the shared agentId namespace.
76
88
  - **Content-safety, strict.** The claim is scanned for prompt injection and refused on a flag regardless of `FLAIR_CONTENT_SAFETY` — an unattended write does not fall back to warn-and-tag.
77
89
  - **Non-impersonating reviewer.** The promoted memory and its candidate record `machine:adk-auto-promote`, never a value mistakable for a human or agent reviewer.
78
90
 
79
91
  Anything ineligible (no scope tag, flagged content, already decided) is left pending for the human `rem promote` path. The step is bounded per cycle and non-fatal; `flair rem nightly run-once` reports the count auto-promoted. **Non-ADK candidates never auto-promote** — the human review gate below is unchanged for them.
92
+
93
+ ## Memory retention and deletion
94
+
95
+ Durability is owner-controlled, including `permanent`: owners may create, change, archive and delete their own memories at every tier. Administrators may delete any owner’s memory. The permanent tier controls retention and bootstrap priority; it does not prevent an explicit owner deletion.
96
+
97
+ Soul is operator-authored identity, not a promotion target for learned candidates. `flair rem promote --to soul` is refused for stored candidate text even with operator credentials; promote learned claims to Memory instead.
package/docs/upgrade.md CHANGED
@@ -8,8 +8,9 @@ target version before upgrading anything you depend on in production.
8
8
 
9
9
  There are two things you might be upgrading:
10
10
 
11
- 1. **A local npm install** — the common case: `flair` running on your own machine or a
12
- VPS, installed via `npm install -g @tpsdev-ai/flair`.
11
+ 1. **A local install** — the common case: `flair` on your own machine or a VPS,
12
+ either via `npm install -g @tpsdev-ai/flair` or a plain extracted tree
13
+ (`npm pack` + `npm install --omit=dev` under systemd).
13
14
  2. **A Flair component deployed to a Harper Fabric cluster** — a different mechanism
14
15
  (`flair deploy` / `flair upgrade --target`), covered separately below.
15
16
 
@@ -43,6 +44,48 @@ transitive dependency). **Other integrations upgrade in their own ecosystem, not
43
44
  `flair upgrade`:** `pi-flair` (pi's plugin manager), `langgraph-flair` / `hermes-flair`
44
45
  (pip / your Python package manager), `n8n-nodes-flair` (n8n's Community Nodes UI).
45
46
 
47
+ If the running instance's exec path is a **plain extracted tree** (npm pack +
48
+ `npm install --omit=dev`, typically under systemd — no git checkout, not the
49
+ npm-global prefix), `flair upgrade` takes that lane: it fetches the published
50
+ tarball, swaps the tree in place, keeps operator launchers that are not in the
51
+ pack, and restarts the systemd unit that points at the tree (or `flair restart`
52
+ when no unit is found). Pass `--tree <dir>` to select the tree explicitly;
53
+ `--flair-version <semver>` pins the tarball.
54
+
55
+ A leftover npm-global relic next to a tree that is **not** a packed install
56
+ still gets the mismatch warning: the listing is the npm-global surface and the
57
+ warning names both paths so the relic is not reported as "the" install. Git
58
+ checkouts and source trees are never tarball-swapped.
59
+
60
+ ### Plain-tree / npm-pack install
61
+
62
+ Spokes that extract the published tarball and run `npm install --omit=dev`
63
+ under a systemd unit — no git checkout, no `npm install -g` — use the same
64
+ command. `flair upgrade` detects the serving tree (or takes `--tree <dir>`)
65
+ and:
66
+
67
+ 1. Fetches `@tpsdev-ai/flair@<version>` with `npm pack`
68
+ 2. Extracts it next to the live tree and runs `npm install --omit=dev`
69
+ 3. Copies operator files at the tree root that are **not** in the published
70
+ pack (a `flair` wrapper, `.env`, anything else you added)
71
+ 4. Renames the live tree to `<tree>.upgrade-prev` and the staging dir into
72
+ place
73
+ 5. Restarts the systemd unit whose `WorkingDirectory` / `ExecStart` names the
74
+ tree (`FLAIR_SYSTEMD_UNIT=flair.service` adds an explicit unit). If no unit
75
+ is found, it falls back to `flair restart`
76
+ 6. Verifies, then removes `.upgrade-prev`. On a failed restart or verify the
77
+ previous tree is swapped back
78
+
79
+ ```bash
80
+ flair upgrade --check --tree /opt/flair
81
+ flair upgrade --tree /opt/flair
82
+ flair upgrade --tree /opt/flair --flair-version 0.50.0
83
+ ```
84
+
85
+ `--check` prints the plan (versions, preserved names, which unit will restart)
86
+ without touching the tree. A git checkout or a path that *is* the npm-global
87
+ install is refused rather than overwritten.
88
+
46
89
  ### Upgrade is a transaction
47
90
 
48
91
  As of flair#635, `flair upgrade` is install → restart → verify →
@@ -557,3 +600,5 @@ you haven't personally tested.
557
600
  shows up when it does.
558
601
  - [`docs/deployment.md`](deployment.md) — initial install / deployment, as opposed to
559
602
  upgrading an existing one.
603
+ - [`docs/api-reference.md`](api-reference.md) — HTTP endpoints, auth per resource, and
604
+ the Presence / Memory / Soul / Agent / Federation schemas.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.51.2",
3
+ "version": "0.53.0",
4
4
  "packageManager": "bun@1.3.10",
5
5
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
6
6
  "type": "module",
@@ -50,7 +50,8 @@
50
50
  "build": "tsc -p tsconfig.json --noCheck && node scripts/write-build-info.mjs",
51
51
  "build:cli": "tsc -p tsconfig.cli.json --noCheck && node scripts/write-build-info.mjs",
52
52
  "prepublishOnly": "npm run build && npm run build:cli",
53
- "test": "bun test",
53
+ "test": "bun run test:unit",
54
+ "test:unit": "bun scripts/test-unit.ts",
54
55
  "test:e2e": "playwright test",
55
56
  "release": "./scripts/release.sh",
56
57
  "postinstall": "node -e \"try{require('./dist/postinstall.cjs')}catch(e){}\""
@@ -68,7 +69,7 @@
68
69
  "harper": "5.2.8",
69
70
  "harper-fabric-embeddings": "^0.5.0",
70
71
  "jose": "6.2.2",
71
- "js-yaml": "^4.3.1",
72
+ "js-yaml": "^4.3.2",
72
73
  "tar": "^7.5.22",
73
74
  "tweetnacl": "1.0.3"
74
75
  },
@@ -77,8 +78,8 @@
77
78
  "brace-expansion": "^5.0.9",
78
79
  "undici": "^8.9.0",
79
80
  "fast-uri": "^4.1.3",
80
- "hono": "^4.12.34",
81
- "js-yaml": "^4.3.1",
81
+ "hono": "^4.13.5",
82
+ "js-yaml": "^4.3.2",
82
83
  "adm-zip": "^0.6.0",
83
84
  "@opentelemetry/core": "^2.8.0",
84
85
  "uuid": "^11.1.1",
@@ -3,6 +3,12 @@ type Memory @table(database: "flair") {
3
3
  agentId: String! @indexed
4
4
  content: String!
5
5
  contentHash: String @indexed
6
+ trigger: String @indexed # flair#1542: the "when to use" text for a skill-tagged row. Skill-tagged
7
+ # rows embed from `trigger` (the recall signal), NOT from `content` (the full
8
+ # procedure) — see resources/skill-write.ts's skillEmbedText. Nullable/additive:
9
+ # non-skill rows carry no trigger and embed from `content` exactly as before
10
+ # (clean-upgrade-path gate). @indexed so the later skill recall tools can filter
11
+ # by trigger without a full-corpus scan.
6
12
  visibility: String
7
13
  embedding: [Float] @indexed(type: "HNSW", M: 16) # M:16 is Harper's own HNSW default
8
14
  # (HierarchicalNavigableSmallWorld M default) — declaring it explicitly is a
@@ -39,8 +45,11 @@ type Memory @table(database: "flair") {
39
45
  createdAt: String! @indexed
40
46
  updatedAt: String
41
47
  expiresAt: String @indexed
42
- retrievalCount: Int
43
- lastRetrieved: String
48
+ retrievalCount: Int # Observable search-hit counter. Authoritative increments live on
49
+ # MemoryHitStat (resources/hit-tracking.ts, flair#1528); GET/search
50
+ # overlay that row so this field still reads as it always has.
51
+ # The Memory row itself is no longer rewritten per hit.
52
+ lastRetrieved: String # Overlay companion to retrievalCount — see MemoryHitStat.
44
53
  usageCount: Int # flair#683: verified-USE signal, distinct from retrievalCount (a
45
54
  # search HIT). Additive/nullable — absent reads as 0, existing rows
46
55
  # unaffected. NEVER auto-incremented on search/retrieval; the ONLY
@@ -149,6 +158,7 @@ type Soul @table(database: "flair") {
149
158
  value: String!
150
159
  priority: String # critical | high | standard | low (skill governance)
151
160
  metadata: String # JSON blob (skill governance: source, version, hash, etc.)
161
+ provenance: String # Server-derived operator/internal source class and authenticated author.
152
162
  durability: String @indexed
153
163
  createdAt: String!
154
164
  updatedAt: String
@@ -191,6 +201,26 @@ type MemoryUsage @table(database: "flair") {
191
201
  createdAt: String!
192
202
  }
193
203
 
204
+ # MemoryHitStat — search hit-tracking ledger (flair#1528 remaining slice).
205
+ # SemanticSearch used to patchRecord the full Memory row per returned hit so
206
+ # retrievalCount/lastRetrieved stayed on the memory itself. That is a full-row
207
+ # Harper put (embeddings included), feeds BM25/HNSW, and loses increments when
208
+ # concurrent searches read-modify-write the same five hits (Dex: 310 vs 320).
209
+ #
210
+ # This table is the increment target: one small row per memory id, coalesced
211
+ # per-id so concurrent hits add rather than race. No @export — no REST surface.
212
+ # resources/hit-tracking.ts is the only writer. Readers that expose the
213
+ # historical Memory.retrievalCount / lastRetrieved contract overlay this row
214
+ # (Memory.get/search, AdminMemory detail, MemoryConsolidate). Existing Memory
215
+ # field values seed the first increment so a store that already had counts
216
+ # does not reset. Agents have no role grant; internal raw-table writes bypass
217
+ # the flair_agent role the same way usage-recording writes MemoryUsage.
218
+ type MemoryHitStat @table(database: "flair") {
219
+ id: ID @primaryKey # Memory.id
220
+ retrievalCount: Int
221
+ lastRetrieved: String
222
+ }
223
+
194
224
  # MemoryCandidate — staged distillations from the FLAIR-NIGHTLY-REM cycle.
195
225
  # Per docs/rem.md. Slice 1 of ops-2qq adds the schema +
196
226
  # `flair rem candidates` listing command. Future slices wire the nightly
@@ -245,3 +275,22 @@ type MemoryCandidate @table(database: "flair") @export {
245
275
  # visibilityRuling — the auditable "why shared" recorded on the candidate.
246
276
  # Nullable/additive, same clean-upgrade contract as scopeTag above.
247
277
  }
278
+
279
+ # Asset — binary blobs (screenshots, images) stored out-of-record via Harper's
280
+ # Blob type, referenced from a Memory by `memoryId`. Slice 1: storage only
281
+ # (no MCP exposure). owner-only + identity-gated + federation "excluded" per
282
+ # resources/record-types.ts (RECORD_TYPES.Asset) — a new type must not enter
283
+ # federation sync (Federation.ts's table list is unchanged).
284
+ # Lifecycle: retained until the owner deletes the Asset row. Deleting the
285
+ # parent Memory does not sweep linked blobs (no GC this slice). Slice 2
286
+ # serving 404s dangling refs; GC + memoryId exist-and-owned check deferred.
287
+ # updatedAt is nullable/additive so a later sweep can key on recency.
288
+ type Asset @table(database: "flair") @export {
289
+ id: ID @primaryKey
290
+ agentId: String! @indexed # owner (no-forge attribution / read scope)
291
+ memoryId: String @indexed # the Memory record this asset belongs to
292
+ contentType: String # MIME type, e.g. image/jpeg
293
+ data: Blob # binary content, stored out-of-record
294
+ createdAt: String! @indexed
295
+ updatedAt: String # stamped on write; later sweep key (nullable/additive)
296
+ }
@@ -0,0 +1,74 @@
1
+ # Message table — Flair Relay S1 (point-to-point signed messaging).
2
+ #
3
+ # A message is a DIFFERENT primitive from memory and from OrgEvent: it is
4
+ # addressed to a recipient PRINCIPAL, ordered per-sender, durably queued, and
5
+ # explicitly acked, with a VISIBLE dead-letter the sender can see. OrgEvent's
6
+ # `expiresAt` is a silent dead-letter; Message never drops silently.
7
+ #
8
+ # The FULL envelope lands in S1 (not just the fields S1 exercises) so that S2
9
+ # (cross-host) is not a schema migration — `orgScope`, `seq` and `contentHash`
10
+ # are covered by the signature and S2's cross-host correctness needs them.
11
+ # See the Flair Relay S1 design (flair#1521) §4.2 and §12.
12
+ type Message @table(database: "flair") @export {
13
+ id: ID @primaryKey
14
+
15
+ # Server-resolved from the auth/pairing context, never trusted from the body
16
+ # (single host: the instance IS the org). Inside the signed canonical body.
17
+ orgScope: String @indexed
18
+
19
+ # Owning principal = the SENDER. Stamped from the authenticated identity
20
+ # (no-forge), and the per-table federation owner field (federation-classify).
21
+ from: String @indexed
22
+ to: String @indexed
23
+
24
+ # Sender-minted, principal-prefixed, globally unique (↔ A2A contextId later).
25
+ threadId: String @indexed
26
+ # Per (from, threadId), monotonic, SIGNED. The recipient sorts by seq at read
27
+ # (createdAt collides at ms resolution). See §12 P1-5.
28
+ seq: Int
29
+
30
+ # "message" for S1 (the degenerate one-shot). "task"/"task-update" are S3.
31
+ kind: String
32
+ # Opaque to Flair.
33
+ body: String
34
+
35
+ # Reply linkage (design §4.2). Lands in S1 so S2/S3 are not a schema
36
+ # migration, though "understood" reply semantics are S3. Both are CONTENT —
37
+ # covered by contentHash AND the signature (relay-lib SIGNED_BODY_FIELDS) —
38
+ # never trusted unsigned. `inReplyTo` is the parent message id; the redundant
39
+ # `parentContentHash` binds the reply to the parent's exact content.
40
+ inReplyTo: String
41
+ parentContentHash: String
42
+
43
+ createdAt: String @indexed
44
+ # Sender-set fail-by. When it passes on an UNCONSUMED message the row goes to
45
+ # a VISIBLE `failed` state (reason "deadline") — the inverse of a silent drop.
46
+ deadline: String
47
+
48
+ # submitted -> delivered -> consumed | failed. `consumed` is ABSORBING —
49
+ # no later write (including a deadline sweep) regresses it (§12 P0-3).
50
+ state: String @indexed
51
+ deliveredAt: String
52
+ consumedAt: String @indexed
53
+ # Dead-letter reason on a `failed` row ("deadline" | "inbox_full" | ...),
54
+ # queryable by the sender.
55
+ failureReason: String
56
+
57
+ # Over the signed canonical CONTENT — retry-send dedup + a second integrity
58
+ # check under the signature (§12 P0-4).
59
+ contentHash: String
60
+
61
+ # Provenance (shared with flair#1520) — "which model sent this".
62
+ senderModel: String
63
+ senderProvider: String
64
+ senderRunId: String
65
+
66
+ # Ed25519 over the canonical SIGNED body — the single source of truth is
67
+ # resources/relay-lib.ts SIGNED_BODY_FIELDS; keep this list in sync with it.
68
+ # It COVERS: the content (from, to, threadId, seq, kind, body, orgScope,
69
+ # deadline, inReplyTo, parentContentHash, senderModel/Provider/RunId) PLUS
70
+ # id, createdAt and contentHash. It EXCLUDES the mutable server/lifecycle
71
+ # fields (state, deliveredAt, consumedAt, failureReason) and this `signature`
72
+ # field itself. (Not "every field above" — the lifecycle fields are NOT signed.)
73
+ signature: String
74
+ }
@@ -0,0 +1,73 @@
1
+ #!/bin/sh
2
+ # start-flair-with-admin-pass.sh — product-owned launchd launcher (flair#1573).
3
+ #
4
+ # Reads the Flair admin password from a 0600 file and execs Harper
5
+ # NON-INTERACTIVELY. The secret never appears in the launchd plist: it enters
6
+ # the process environment here, from the file, at start time. This is the
7
+ # product shape of the no-inline-secret pattern — the plist's ProgramArguments
8
+ # point at this launcher instead of embedding HDB_ADMIN_PASSWORD.
9
+ #
10
+ # Usage: start-flair-with-admin-pass.sh <admin-pass-file> <node> <harper-bin>
11
+ #
12
+ # admin-pass-file path to the 0600 file holding the admin password
13
+ # node the node binary to exec
14
+ # harper-bin Harper's entrypoint (harper.js)
15
+ #
16
+ # HDB_ADMIN_USERNAME is already in the launchd environment (it is not a
17
+ # secret); only the password is read from the file here. HOME, PATH and the
18
+ # Harper config (HARPER_SET_CONFIG / ROOTPATH / FLAIR_MODELS_DIR) are also
19
+ # supplied by the plist's EnvironmentVariables, so Harper boots without ever
20
+ # hitting its interactive readline prompt under launchd's minimal env.
21
+
22
+ set -eu
23
+
24
+ ADMIN_PASS_FILE="$1"
25
+ NODE="$2"
26
+ HARPER_BIN="$3"
27
+
28
+ if [ ! -f "$ADMIN_PASS_FILE" ]; then
29
+ echo "start-flair-with-admin-pass: admin-pass file not found: $ADMIN_PASS_FILE" >&2
30
+ exit 1
31
+ fi
32
+
33
+ if [ ! -r "$ADMIN_PASS_FILE" ]; then
34
+ echo "start-flair-with-admin-pass: admin-pass file not readable: $ADMIN_PASS_FILE" >&2
35
+ exit 1
36
+ fi
37
+
38
+ # Re-verify owner-only (0600) at READ time, not just at `flair init` write
39
+ # time. A file that drifted to 0644 after init (umask change, backup tool,
40
+ # tar restore) would leak the secret to any reader on the host. Mirrors
41
+ # readSecretFileSecure (src/lib/auth-resolve.ts), which refuses any group/other
42
+ # permission bit. `stat -f %Lp` is macOS (the launchd host); `stat -c %a` is
43
+ # Linux (the unit-test host). The two syntaxes are mutually exclusive, so
44
+ # branch on the OS rather than chaining with `||` — on Linux `stat -f %Lp`
45
+ # prints filesystem info to stdout *and* exits non-zero, which would pollute
46
+ # the captured mode. Fail CLOSED: an unreadable mode (empty) refuses rather
47
+ # than proceeding on a guess.
48
+ case "$(uname -s)" in
49
+ Darwin) MODE="$(stat -f %Lp "$ADMIN_PASS_FILE" 2>/dev/null)" ;;
50
+ *) MODE="$(stat -c %a "$ADMIN_PASS_FILE" 2>/dev/null)" ;;
51
+ esac
52
+ case "$MODE" in
53
+ *00) : ;;
54
+ *)
55
+ echo "start-flair-with-admin-pass: admin-pass file permissions '${MODE:-unknown}' are too open (expected 600): $ADMIN_PASS_FILE" >&2
56
+ exit 1
57
+ ;;
58
+ esac
59
+
60
+ # Read the secret. Command substitution strips a trailing newline, which is
61
+ # what `flair init` writes (base64url + "\n"); the value itself is preserved
62
+ # verbatim by the double quotes.
63
+ ADMIN_PASS="$(cat "$ADMIN_PASS_FILE")"
64
+ if [ -z "$ADMIN_PASS" ]; then
65
+ echo "start-flair-with-admin-pass: admin-pass file is empty: $ADMIN_PASS_FILE" >&2
66
+ exit 1
67
+ fi
68
+
69
+ export HDB_ADMIN_PASSWORD="$ADMIN_PASS"
70
+
71
+ # exec (not spawn) so launchd tracks Harper itself — the job's PID is Harper's
72
+ # PID, and KeepAlive restarts the real service rather than a dead launcher.
73
+ exec "$NODE" "$HARPER_BIN" run .