@passioncode-ai/fabric-agent-adapter 0.58.0 → 0.60.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/CHANGELOG.md +97 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +4 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +27 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +16 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +35 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +80 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +11 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +80 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +14 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +187 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +2 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +19 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +27 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +23 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +5 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +21 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.60.0 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
The board responder is ready to find a slot the operator moves back to it, once Fabric offers the move (FAA-165), and four smaller fixes: working-with-fabric
|
|
6
|
+
reads a filed row's answer (FAA-169) and says when an accepted request is left behind by a move (FAA-172), `pack`
|
|
7
|
+
names why a provider manifest is refused (FAA-193), and Python's package kit refuses `NaN` and `Infinity` as Node's does.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- A slot the operator moved to another session and then back to this one (contract DEC-0140 PR-2, once Fabric offers
|
|
12
|
+
the move) would be held but never claimed or renewed: the responder skipped a `moved` slot until `take_back`, so its lease lapsed. Now, while a slot
|
|
13
|
+
is moved, the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the recorded
|
|
14
|
+
move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says. A claim that lands
|
|
15
|
+
holds the slot again (the record loses `moved`, the request it handed out is worked, or its own enroll renews the
|
|
16
|
+
lease at once; a later move is said again); `lease_expired` enrolls again; `held` asks at the next renewal;
|
|
17
|
+
`idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a fresh key, recording
|
|
18
|
+
nothing; `fenced` or another refusal that is not the board session's records the new generation, so the same move is not asked about
|
|
19
|
+
twice. A recorded move drops the capability's claim key, so a claim answer lost before the move is never replayed, and a
|
|
20
|
+
lost answer to the move-back claim itself is asked again under its key, counting as a lease from its send meanwhile (while more than the 10 s margin of that lease is left)
|
|
21
|
+
(a replay from an older generation is dropped); `not_authorized` counts as another session's. It never enrolls or replaces a
|
|
22
|
+
slot another session holds, and waits while other work runs. Both kits (`fabric_board_slots.py`,
|
|
23
|
+
`fabric_board_session.py` and their Node twins). The `board.slot.moved` notice and references/board.md say so (FAA-165).
|
|
24
|
+
- `pack`, `sign` and `verify` refused a provider manifest whose `costPresets` differ from the package's with only
|
|
25
|
+
`provider-manifest` as the message; the message now names both causes (the `problem` code is unchanged) (not a JSON object, or `costPresets` that differ). Both kits (FAA-193, found by the
|
|
26
|
+
FAA-124 pilot rehearsal). Python no longer refuses an empty provider manifest (`{}`), as Node never did (FAA-194 (1)).
|
|
27
|
+
- Python's `pack`, `sign` and `verify` took `NaN`, `Infinity` and `-Infinity` in a plugin or provider manifest, a key file
|
|
28
|
+
or a trust file, which `JSON.parse` and the contract refuse, so a package the Node kit refused could pass Python's
|
|
29
|
+
`verify`. Python refuses them now too (older than 0.60.0, since 0.31.0; found by the 0.60.0 seam reading 11).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- working-with-fabric Step 6 reads `fabric_task_create`'s answer as Step 4 item 2 does: while your single live claim is held, Fabric
|
|
34
|
+
0.3.14 links what is filed as spawned from it, so an unrelated row is filed after the release, or the person is told
|
|
35
|
+
it is linked (FAA-169). A scenario eval `a-backlog-row-filed-while-a-claim-is-held`.
|
|
36
|
+
- working-with-fabric references/project-board.md, "When nobody answers": an `active` responder does not prove a request
|
|
37
|
+
is worked; one the old holder accepted before its slot moved stays `accepted` with nobody on it, and a requester
|
|
38
|
+
cannot see that (`com.get` shows no attempt), so it asks on the thread; with no answer it cancels the request while
|
|
39
|
+
its `effect` is `not_started` and goes on only once the cancel's receipt shows `cancelled`. An accepted request is
|
|
40
|
+
never adapted by hand and never expires (FAA-172; question to Fabric and the contract on board thread `ab12642e`). A
|
|
41
|
+
scenario eval `an-accepted-request-left-behind-by-a-move`.
|
|
42
|
+
- building-fabric-services SKILL.md: a moved slot stays moved until `take_back` or a move back once Fabric offers one.
|
|
43
|
+
- The reference service's take-back tool now reads "Take back a board slot moved to another session."; a manifest
|
|
44
|
+
registered before 0.60.0 keeps the old description until it is registered again.
|
|
45
|
+
|
|
46
|
+
### Upgrading
|
|
47
|
+
|
|
48
|
+
- No Fabric release offers the operator's move of a slot yet (Fabric 0.3.15 has none): today a moved slot is
|
|
49
|
+
another session's taking it, and take-back is the way back; the move-back check is ready for when Fabric ships it.
|
|
50
|
+
- A kit service gets the move back once it installs the 0.60.0 kit modules (`fabric_board_slots.py`,
|
|
51
|
+
`fabric-board-slots.mjs` and their callers). Only the running session finds a move back: a slot moved back to a
|
|
52
|
+
session that has since ended needs take-back, since the session's end released the slot and the operator cannot move
|
|
53
|
+
a slot no session holds (a move is possible only once another session holds it). A kit older than 0.60.0 never finds
|
|
54
|
+
a move back: it needs take-back (any kit from 0.53.0), or, once Fabric offers the move, the 0.60.0 modules and then a move to the new session.
|
|
55
|
+
|
|
56
|
+
## 0.59.0 (2026-11-03)
|
|
57
|
+
|
|
58
|
+
Portable from day 0 (FAA-191, RM-36): on top of 0.58.0's source and instance, every new agent gets a data home, a
|
|
59
|
+
data kind and an uptime category, and a new private agent a `LICENSE` naming its copyright holder, from its first commit, and runs the transfer
|
|
60
|
+
audit at every release until the contract's export (CT-194) replaces it.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- creating-fabric-agents' intake grill asks three more rows: **Does it run around the clock?** (uptime category A, a
|
|
65
|
+
cloud host from the first release, Heroku unless a reason names another, from the cloud Fabric-server template once
|
|
66
|
+
its repository exists; or B, a local service only), **Whose data does it keep?** (kind U, data of people outside the
|
|
67
|
+
owner's team, never in the source, a copy or its tests; or O; in doubt U) and **Who holds the copyright?**
|
|
68
|
+
- Step 2 gives the data a home of its own, `<agent>-data` (a folder beside the checkout or a private repository of
|
|
69
|
+
that name), and the README names the data kind and the uptime category; an agent someone builds for themselves gets
|
|
70
|
+
a `LICENSE` naming the holder (all rights reserved unless the owner gives other terms).
|
|
71
|
+
- Step 5: every release runs `fabric_source_check.py check` as the transfer audit, in Git mode on the checkout and
|
|
72
|
+
with `--every-file` on a clean copy of what Git would commit (building-fabric-services' `references/package.md`).
|
|
73
|
+
references/source-and-instance.md adds why never the checkout (its ignored configuration and dependency folders
|
|
74
|
+
`--every-file` reads too), and that a `LICENSE` whose holder is on the owner's list is named in
|
|
75
|
+
`.fabric-source-ignore`, since authorship travels with the source.
|
|
76
|
+
- references/source-and-instance.md (both skills): *The data home and its kind*; references/where-agents-run.md:
|
|
77
|
+
*Uptime: A or B* and a checklist line. building-fabric-services' Step 0 answers carry the same (data kind, data
|
|
78
|
+
home, uptime, holder, the audit per release).
|
|
79
|
+
- Scenario evals `portable-from-day-zero` (creating-fabric-agents) and `new-service-names-data-and-uptime`
|
|
80
|
+
(building-fabric-services), a trigger, and `test/test_portable_from_day_zero_prose.py` pinning the prose to them.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- The eval `own-agent-licence-is-the-owners-choice` expects a `LICENSE` naming the holder; still no AGPL or
|
|
85
|
+
commercial licence files unasked. The shared operator-channel reference and building-fabric-services say the same
|
|
86
|
+
for an agent built new, and that the kit files the adapter copies in keep their own licence
|
|
87
|
+
(`AGPL-3.0-only OR LicenseRef-PassionCode-Commercial`); adapting-projects-to-fabric leaves an existing agent's
|
|
88
|
+
licence as it is.
|
|
89
|
+
- where-agents-run.md: Heroku is the default host of an A agent unless it runs in the person's or an organization's
|
|
90
|
+
PassionCode cloud or is sold; an A agent's data lives in a durable store, never the host's own folder (source-and-instance.md's
|
|
91
|
+
data home says the same for a host that does not keep its disk), and on a host with no Fabric hub it answers only
|
|
92
|
+
what reaches its own origin.
|
|
93
|
+
- building-fabric-services' gotchas are shorter (the same rules); `test_fabric_kit` runs its by-path load with `-B`.
|
|
94
|
+
|
|
95
|
+
### Upgrading
|
|
96
|
+
|
|
97
|
+
- Nothing to change for an existing agent: an adapted project keeps its licence, data and release checks as they
|
|
98
|
+
are. A new one answers the three grill rows before its first commit.
|
|
99
|
+
|
|
3
100
|
## 0.58.0 (2026-11-03)
|
|
4
101
|
|
|
5
102
|
Portable agents, part 1 (FAA-188, RM-36): an agent keeps its source apart from its instance from its first commit, and
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ requests other Projects post to its Project on Fabric's project board uses the b
|
|
|
72
72
|
(`scripts/fabric_hub.py` and `scripts/fabric_board.py`, Node `fabric-hub.mjs` and `fabric-board.mjs`): the
|
|
73
73
|
`board.respond` grant, enroll, claim, accept the exact digest, complete, a checkpoint before every step, and restarts
|
|
74
74
|
that reconcile or take accepted work back by a transfer, unless the slot was moved to another session (until the
|
|
75
|
-
operator's take-back) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
|
|
75
|
+
operator's take-back, or a move back once Fabric offers one) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.60.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.60.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
11
11
|
---
|
|
@@ -242,7 +242,8 @@ relayed, DEC-0121). An agent with neither stops nor notify events skips this ste
|
|
|
242
242
|
repository standard (`knowledge/repository-standard.md`) from its first commit: the licence files,
|
|
243
243
|
the README quick start verified with a real client, `AGENTS.md` and `CLAUDE.md`, listed in the
|
|
244
244
|
shared reference above. An agent someone builds for themselves is not a PassionCode.ai repository:
|
|
245
|
-
its licence is its owner's choice and none of those files is required of it
|
|
245
|
+
its licence is its owner's choice and none of those files is required of it (an agent built new with
|
|
246
|
+
creating-fabric-agents carries a `LICENSE` naming its holder; an existing one keeps what it has).
|
|
246
247
|
|
|
247
248
|
## Completion format
|
|
248
249
|
|
|
@@ -63,4 +63,7 @@ the project).
|
|
|
63
63
|
org-index `scripts/check_format.py` reports 0 findings for it before it is called done. An agent
|
|
64
64
|
someone builds for themselves is not a PassionCode.ai repository: its licence is its owner's
|
|
65
65
|
choice, nothing about it is published or listed by the organization, and none of these files is
|
|
66
|
-
required of it,
|
|
66
|
+
required of it, except that an agent built new carries a `LICENSE` naming the copyright holder (all rights reserved
|
|
67
|
+
unless the owner gives other terms), as does a new private agent built for a client; files the adapter copies in (the service kit, the dashboard kit) keep their own
|
|
68
|
+
licence, `AGPL-3.0-only OR LicenseRef-PassionCode-Commercial` ([COMMERCIAL-LICENSE.md](https://github.com/passioncode-ai/fabric-agent-adapter/blob/main/COMMERCIAL-LICENSE.md)).
|
|
69
|
+
The verified MCP quick start is still how anyone learns to drive it.
|
|
@@ -13,6 +13,7 @@ on it.
|
|
|
13
13
|
## Contents
|
|
14
14
|
|
|
15
15
|
- [The layout](#the-layout)
|
|
16
|
+
- [The data home and its kind](#the-data-home-and-its-kind)
|
|
16
17
|
- [The check](#the-check)
|
|
17
18
|
|
|
18
19
|
## The layout
|
|
@@ -31,7 +32,7 @@ on it.
|
|
|
31
32
|
(an organization's) is a sibling instance, `<id>.<instance>`, never a folder inside the personal one
|
|
32
33
|
(`references/estate-record.md` of that skill).
|
|
33
34
|
- **Ignore the instance in the source**: `.gitignore` names the real configuration (`config/*.json` with
|
|
34
|
-
`!config/*.example.json`), an instance folder
|
|
35
|
+
`!config/*.example.json`), an instance folder an existing project still keeps inside the checkout, and what running
|
|
35
36
|
the agent leaves (`__pycache__/`, `*.pyc`), which the check would otherwise report as binary in Git mode.
|
|
36
37
|
- **State documents are instance data.** A handoff, a decision log or a backlog about this codebase is source
|
|
37
38
|
documentation; a record of what this installation did (runs, conversations, the owner's notes, a journal) lives in
|
|
@@ -47,6 +48,27 @@ on it.
|
|
|
47
48
|
keeps the scaffold's sentinel, and a person's name or account is never written there. The adapter moves both when the
|
|
48
49
|
contract decides how an instance renames them (FAA-188 part 2).
|
|
49
50
|
|
|
51
|
+
## The data home and its kind
|
|
52
|
+
|
|
53
|
+
- **The data has a home of its own from the first commit: `<agent>-data`.** For an agent that is not a kit service it
|
|
54
|
+
is a folder beside the checkout, never inside it, and `<AGENT>_DATA_DIR` names it (its default is
|
|
55
|
+
`../<agent>-data`); a kit service keeps the kit's folders above. When the owner keeps the data in Git, it goes to a
|
|
56
|
+
separate private repository named `<agent>-data`, never to the source's repository. On a cloud host that does not
|
|
57
|
+
keep its disk across a restart (Heroku and the like, an uptime A agent's default), the data home is a durable store
|
|
58
|
+
the same variable names (a database, an object store, that private repository); the folder and the kit's folders
|
|
59
|
+
serve a machine whose disk persists. A copy of the agent starts with an empty home of its own and never connects
|
|
60
|
+
to the original's.
|
|
61
|
+
- **The README names the kind of the data.** **U**: the agent keeps data of people outside the owner's team
|
|
62
|
+
(messages, support requests, reviews, customers' answers); it never enters the source, a copy or its tests, and the
|
|
63
|
+
source holds only the schema and synthetic fixtures (its real-trace corpus and replay samples live in the data
|
|
64
|
+
home). **O**: it keeps none (a tool or service for other agents, the
|
|
65
|
+
owner's reports, runs and media); its working data still goes to the data home. When in doubt, the kind is U.
|
|
66
|
+
- **Every release runs the transfer audit.** `fabric_source_check.py check <project-root>` in Git mode, and
|
|
67
|
+
`check <clean-copy> --every-file` on a clean copy of what Git would commit (`git ls-files --cached --others
|
|
68
|
+
--exclude-standard`, as building-fabric-services' `references/package.md` makes it; never the checkout itself, whose ignored
|
|
69
|
+
configuration and dependency folders `--every-file` reads too), each exit 0, with the owner's list or its absence recorded, until the contract's export (CT-194)
|
|
70
|
+
replaces it.
|
|
71
|
+
|
|
50
72
|
## The check
|
|
51
73
|
|
|
52
74
|
`fabric_source_check.py check` reads the files the source would carry (what Git would commit: tracked files and
|
|
@@ -124,7 +146,10 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/fabric_source_ch
|
|
|
124
146
|
repository is not read; `--every-file` and a folder outside Git read its files, its `.git` left out. An exception line (`!`) is refused, since what it would
|
|
125
147
|
bring back would go unread. Name the files, or a folder that holds
|
|
126
148
|
only synthetic vectors, never a whole source folder (a nested repository checked in its own tree is the one
|
|
127
|
-
exception), and say why on the line above.
|
|
149
|
+
exception), and say why on the line above. The `LICENSE` is the other: its copyright line names the holder, which is
|
|
150
|
+
authorship and travels with the source, so when the holder is on the owner's list the licence file is named in the ignore
|
|
151
|
+
file by its own path (`LICENSE`, `LICENSE.md`) with that reason (`# the copyright holder is authorship and travels with the source`); no other file of the
|
|
152
|
+
owner's is. A file `pack` refuses by its name cannot ship in a package: a public
|
|
128
153
|
certificate is renamed `*.crt`; a project `.npmrc` that holds no credential may be named in the ignore file, with
|
|
129
154
|
that reason, for the source check (the line then hides whatever is added to it later, so keep it credential-free),
|
|
130
155
|
and `pack` still refuses it, so a package is made from a copy without it (its
|
|
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
14
14
|
compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
15
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.60.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
@@ -63,6 +63,7 @@ Answer before writing code, and record the answers in the project README:
|
|
|
63
63
|
| Port | one number; run the claim check below before choosing it |
|
|
64
64
|
| Who calls it | the named agents or people, and through which surface (Step 1) |
|
|
65
65
|
| What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
|
|
66
|
+
| Data and uptime | kind U or O, kit folders, a durable store or `<id>-data`, uptime A or B; a new agent's `LICENSE` holder, transfer audit per release (creating-fabric-agents) |
|
|
66
67
|
| What it tells the operator | the events worth a notification (Step 5) |
|
|
67
68
|
|
|
68
69
|
## Step 1: choose the surfaces
|
|
@@ -102,9 +103,9 @@ When the service answers requests other Projects post to its Project on Fabric's
|
|
|
102
103
|
responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) finds the hub and joins,
|
|
103
104
|
[`scripts/fabric_board.py`](scripts/fabric_board.py) enrolls, claims, accepts the exact digest, checkpoints before
|
|
104
105
|
every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
|
|
105
|
-
The request body is data behind the service's own input gate.
|
|
106
|
-
session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate
|
|
107
|
-
|
|
106
|
+
The request body is data behind the service's own input gate. A slot moved to another
|
|
107
|
+
session stays moved until `take_back` (`takeBack`) or a move back once Fabric offers one: give it an operator action with its tool and gate
|
|
108
|
+
`relayed` (the operator reaches it through an agent that relays their decision in `_meta`, the service's
|
|
108
109
|
page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
|
|
109
110
|
|
|
110
111
|
## Step 2: the non-negotiables
|
|
@@ -353,9 +354,9 @@ first commit:
|
|
|
353
354
|
|
|
354
355
|
Whoever owns the repository, an agent with a page gets the dashboard rule block in its `AGENTS.md`
|
|
355
356
|
(below). org-index `scripts/check_format.py` reports 0 findings for it before it is called done. **An agent
|
|
356
|
-
someone builds for themselves is not a PassionCode.ai repository:** its licence is its owner's
|
|
357
|
-
|
|
358
|
-
|
|
357
|
+
someone builds for themselves is not a PassionCode.ai repository:** its licence is its owner's choice, the organization
|
|
358
|
+
publishes and lists nothing about it, of these files one built new needs only a `LICENSE` naming its copyright holder (copied kit files keep
|
|
359
|
+
theirs), and the verified MCP quick start teaches anyone to drive it.
|
|
359
360
|
|
|
360
361
|
## Online services: the remote placement
|
|
361
362
|
|
|
@@ -418,20 +419,16 @@ this estate needs.
|
|
|
418
419
|
## Gotchas
|
|
419
420
|
|
|
420
421
|
- `lsof` shows `*:<port>` or `0.0.0.0` → the bind is wrong even if the Host check works.
|
|
421
|
-
- A health probe that only checks HTTP 200 accepts any program on the port
|
|
422
|
-
`service.id` and `service.instance
|
|
423
|
-
|
|
424
|
-
- `launchctl bootstrap` right after `bootout` fails with I/O error 5 until the unload
|
|
425
|
-
finishes; wait, then retry.
|
|
422
|
+
- A health probe that only checks HTTP 200 accepts any program on the port: compare
|
|
423
|
+
`service.id` and `service.instance`.
|
|
424
|
+
- `launchctl bootstrap` right after `bootout` fails with I/O error 5: wait for the unload, then retry.
|
|
426
425
|
- Under launchd `PATH` is bare. Pin the interpreter by absolute path; set `PATH`
|
|
427
426
|
explicitly in the plist; never copy the whole shell environment into it.
|
|
428
|
-
- A plist pointing at a worktree or
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
- `logging.basicConfig` called twice: the second call is a no-op, so a server's own
|
|
434
|
-
log file stays empty. Configure logging once, in the entry point.
|
|
427
|
+
- A plist pointing at a worktree or checkout breaks when it moves: point it at a release.
|
|
428
|
+
- `http.server.HTTPServer` resolves its FQDN between `bind()` and `listen()`: with a slow
|
|
429
|
+
resolver (a macOS CI runner, a Mac offline) the port is bound but silent. Use `fs.LoopbackHTTPServer`.
|
|
430
|
+
- `logging.basicConfig` called twice: the second is a no-op, the server's log file
|
|
431
|
+
stays empty. Configure logging once, in the entry point.
|
|
435
432
|
- Hand-made `.plist.bak` files in `~/Library/LaunchAgents` are never cleaned up; keep
|
|
436
433
|
backups elsewhere.
|
|
437
434
|
|
|
@@ -120,7 +120,7 @@ once the kit reads it (FAA-135).
|
|
|
120
120
|
|---|---|
|
|
121
121
|
| `fenced` | the attempt is another responder's: stop the handler, drop the entry (nothing outside had begun); accepted work on a slot this responder recorded holding stays as `lapsed`, and the next tick asks `com.status` whether the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155) |
|
|
122
122
|
| `lease_expired` on accepted work | stop the handler; the board holds the request, and the kit takes it back with `com.responder_replace` and a transfer |
|
|
123
|
-
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for |
|
|
123
|
+
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for (except `not_authorized` on the move-back claim of a moved slot, read as another session's slot, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
124
124
|
| `fenced` on enroll or claim | another live holder: wait for it to lapse; never replace it on its own initiative. When this responder recorded holding the slot, it first reads `com.status`: a newer generation means the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155), and it is not taken again even after that holder lapses |
|
|
125
125
|
| lost after an effect began | keep the entry as `unknown`; `observe(entry)` is asked to settle it with `com.reconcile` |
|
|
126
126
|
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning |
|
|
@@ -134,7 +134,7 @@ the target Project's participant reply answered it, DEC-0098 and DEC-0102; not s
|
|
|
134
134
|
it, but no longer this responder's); `in_progress` and `outcome_unknown` are
|
|
135
135
|
reconciled from what `observe(entry)` answers (observed only; with no observation they stay unknown and notify);
|
|
136
136
|
accepted ones go back to this responder with `com.responder_replace {expectedGeneration, transfer}` (unless the slot
|
|
137
|
-
moved to another session meanwhile: they stay `lapsed` until take-back, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)), whose receipt's
|
|
137
|
+
moved to another session meanwhile: they stay `lapsed` until take-back or a move back to this session, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)), whose receipt's
|
|
138
138
|
`transferred` (DEC-0078: `{request, revision, attempt}`) carries the new fence. **A Fabric that does not answer
|
|
139
139
|
`transferred` yet leaves the request held** (`board.request.held`, notify): the kit never claims it back, because the
|
|
140
140
|
contract has no `com.claim` read-back and `com.claim` never moves an accepted request. A known outcome is sent after the
|
|
@@ -181,7 +181,7 @@ contract's matrix, held to its vectors (`test/fixtures/contract/comms-hold-vecto
|
|
|
181
181
|
| Refused `held` | What the kit does |
|
|
182
182
|
|---|---|
|
|
183
183
|
| `com.status` names the slot `held: true` at connect | the slot is not enrolled; it pauses |
|
|
184
|
-
| `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again |
|
|
184
|
+
| `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again (except the move-back claim of a moved slot, which asks again at the next renewal with no `board.slot.held`, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
185
185
|
| `com.accept` (the hold came after the claim) | nothing was done: the entry is dropped, the claim lapses and the request waits on the board (`board.request.waiting`); the slot pauses |
|
|
186
186
|
| `com.effect_begin` | the effect has not begun: the handler waits (same idempotency key, the pause doubling) while the tick keeps renewing the lease, then begins once the hold is lifted; `ctx.stopped` ends the wait |
|
|
187
187
|
| accepted work at restart under a hold | kept as `lapsed`, then taken back with `com.responder_replace` and a transfer once the hold is lifted, never claimed back |
|
|
@@ -194,7 +194,7 @@ whose responder was gone. Read it as the owner's attestation, not the responder'
|
|
|
194
194
|
|
|
195
195
|
## When the operator moves a slot to another session (FAA-155)
|
|
196
196
|
|
|
197
|
-
The contract's DEC-0140 PR-2 (
|
|
197
|
+
The contract's DEC-0140 PR-2 (accepted 2026-10-30, after this kit's pin) lets the operator move a
|
|
198
198
|
(Project, capability) slot to another live session of the Project: one transition, as `com.responder_replace` would make
|
|
199
199
|
it for the receiving session (generation up, `claimed` back to `queued`, `in_progress` to `outcome_unknown`, `accepted`
|
|
200
200
|
held and not transferred). The responder keeps that move instead of undoing it:
|
|
@@ -228,17 +228,18 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
228
228
|
this service left unanswered, is a move, whatever made it: the operator, or another session that took the slot while
|
|
229
229
|
this one was away or after it crashed. Before 0.53.0 a restarted responder took such a slot back by itself. So a
|
|
230
230
|
second responder of the same capability is no longer a standby once the slot changed hands: after any change of
|
|
231
|
-
holder it waits for take-back, not for the other's lapse.
|
|
231
|
+
holder it waits for take-back or a move back to its session, not for the other's lapse.
|
|
232
232
|
- A `fenced` refusal on accepted work while the slot did not move (the generation unchanged) keeps the entry
|
|
233
233
|
`lapsed`; the next tick takes it back with a transfer and goes on (before 0.53.0 the entry was dropped and the
|
|
234
234
|
request left accepted on the board).
|
|
235
235
|
- Then the slot is `moved`: the running handler is told to stop (`ctx.stopped`); a `claimed` entry is dropped (the move
|
|
236
236
|
returned it to the queue); accepted work stays as `lapsed` and work whose effect began as `unknown` (only the slot's
|
|
237
|
-
holder, or the estate owner, reconciles it now).
|
|
238
|
-
`com.
|
|
237
|
+
holder, or the estate owner, reconciles it now). While the slot is moved nothing but the move-back check
|
|
238
|
+
(`com.status` once a renewal, then one `com.claim`, below) is sent for that capability: no `com.renew`,
|
|
239
|
+
`com.complete`, `com.enroll` or `com.responder_replace`, not at recovery after a restart, and not when the new
|
|
239
240
|
holder's lease lapses, nor when com.status later names no generation for the slot (a recorded move stays until
|
|
240
|
-
take-back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back
|
|
241
|
-
will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
241
|
+
take-back or a move back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back
|
|
242
|
+
or moved back (CT-175 will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
242
243
|
(the record keeps `moved`).
|
|
243
244
|
- A responder that never held the slot (a first start: no record, or a record of another Project), or whose record
|
|
244
245
|
equals the current generation, behaves as before: it enrolls, recovers and replaces its own lapsed work. A recorded or
|
|
@@ -248,7 +249,29 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
248
249
|
records its own generation. Only a replay the board refuses, or one that ran for the first time (`repeated: false`)
|
|
249
250
|
after the slot had moved past the record, reads as a move: the safe side; a slot such a replay took is left to lapse
|
|
250
251
|
unused. A generation written `2.0` is `2`.
|
|
251
|
-
-
|
|
252
|
+
- The operator may move the slot back to this session in Fabric (DEC-0140 PR-2), which this client cannot hear. No
|
|
253
|
+
Fabric release offers the operator's move yet (Fabric 0.3.15 has none: the control is CT-174's in Fabric, its agent twin CT-181 in the contract, then CT-125),
|
|
254
|
+
so today every move is another session taking the slot and take-back is the route back; the check below is ready
|
|
255
|
+
for the move. While
|
|
256
|
+
a slot is moved the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the
|
|
257
|
+
recorded move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says (FAA-165,
|
|
258
|
+
0.60.0); the move itself drops the claim key of that capability, whose receipt the move made dead. A claim that
|
|
259
|
+
lands is the slot held again: the record loses `moved`, the request it handed out is worked (with none, its own
|
|
260
|
+
enroll renews the lease at once), accepted work left `lapsed` comes back with a transfer, and a later move is said
|
|
261
|
+
again. `lease_expired` (the move reached this session and its lease lapsed first) enrolls again. `held` asks again
|
|
262
|
+
at the next renewal. `idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a
|
|
263
|
+
fresh key, recording nothing. `fenced`, or any other refusal that is not the board session's (`not_authorized` counted with them, since a
|
|
264
|
+
non-holder's claim may be refused so), is another session's slot:
|
|
265
|
+
recorded as `moved` at that generation, so it is not asked about twice. A claim whose answer was lost (`unreachable`) is asked again under the same key at the next renewal, so a claim that
|
|
266
|
+
landed is not stranded; until then it counts as at most a lease from that key's first send (a fresh key starts
|
|
267
|
+
its own, and it counts only while more than the 10 s margin of that lease is left, then no more, as a lapsed slot), so a second lost answer keeps
|
|
268
|
+
the session (finding 38). A replayed receipt from a generation older than `com.status` shows (a move away and back meanwhile) is
|
|
269
|
+
dead: its key is dropped and a fresh key asks at the next renewal. A take-back drops the capability's claim key too, so a request a move-back claim handed out while the take-back raced
|
|
270
|
+
it waits for its attempt's lease (60 s) before it is claimed again (a take-back that lands before the move-back check makes its claim key
|
|
271
|
+
leaves that key to the claim loop, which works the request at once). Nothing is asked while the responder stops. It is never an enroll or a replace on a slot another session holds, and it waits
|
|
272
|
+
while other work runs: a move back during a job longer than the lease (60 s) may lapse and be lost to another
|
|
273
|
+
session, after which take-back, or another move back to this session, brings it back.
|
|
274
|
+
- Otherwise only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record, and wins over a move
|
|
252
275
|
being read at the same time: the move is written under the responder's lock (Node: on the fence queue, after any fenced
|
|
253
276
|
call in flight) and only while the record is unchanged (FAA-158). Never edit the
|
|
254
277
|
checkpoint's `slots` record by hand to undo a move. In the same board
|
|
@@ -269,9 +292,8 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
269
292
|
tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so a call
|
|
270
293
|
without the operator's relayed decision is refused; on a server that reads mandates (`agent_id`) it also names its effect class, for example
|
|
271
294
|
`change-policy` (DEC-0125); the operator reaches it through an agent that relays their decision in `_meta`, the
|
|
272
|
-
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved
|
|
273
|
-
|
|
274
|
-
surface.
|
|
295
|
+
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved for good today (no Fabric release offers the operator's move back, above). The
|
|
296
|
+
hand-over of accepted requests to another session (contract CT-175) waits for Fabric's session surface.
|
|
275
297
|
|
|
276
298
|
## What a board session may call
|
|
277
299
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -87,7 +87,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
87
87
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
88
88
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
89
89
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
90
|
-
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move
|
|
90
|
+
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move, its take-back and a move back (FAA-165), one capability's refusal retried alone (FAA-163) |
|
|
91
91
|
| L4 | `fabric_board_session` | the board responder: the work context, the fenced request, transfer and recovery |
|
|
92
92
|
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
93
93
|
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
@@ -75,9 +75,14 @@ see a swap made and undone between two of those checks. Pack from a folder only
|
|
|
75
75
|
Then every problem is listed, each with its `rule`: `schema` (the contract's schema), `FAC-SEM-064` (whole: its own
|
|
76
76
|
digest, each path once, every named file listed with the same SHA-256), `FAC-SEM-065` (no secret), `FAC-SEM-066`
|
|
77
77
|
(`agentRef` derived), `FAC-SEM-102` (one role, one server) and `folder` (`file-missing`, `file-digest`, `file-unlisted`,
|
|
78
|
-
`plugin-manifest`, `provider-manifest
|
|
78
|
+
`plugin-manifest`, `provider-manifest`, whose message names its two possible causes: a provider manifest that is not a JSON object,
|
|
79
|
+
or `costPresets` that differ from the package's, FAA-193) and `path` (a listed or folder path holding a control character, U+2028 or
|
|
79
80
|
U+2029, a `..` segment, a backslash, `//` or a colon, refused by the kit itself whatever the schema's pattern lets
|
|
80
|
-
through, FAA-78).
|
|
81
|
+
through, FAA-78). `fabric.package.json`, the trust file and a JWK key file take no `NaN` or `Infinity` token (a number too large for a double still reads as an infinity, in both kits as in `JSON.parse`), and the first two are read as strict UTF-8 and nested at most 256 levels; the plugin and
|
|
82
|
+
provider manifests the folder check reads are strict UTF-8 too (the contract's `checkPackageFolder` decodes them with
|
|
83
|
+
replacement characters, so an invalid byte inside a string value the check does not compare is no manifest here and is accepted there, FAA-194 (29)), take no `NaN` or `Infinity` token either, as `JSON.parse` takes none, and have no depth
|
|
84
|
+
limit of their own, as the contract's reference has none (Python 3.9's own recursion limit reads one nested about 990
|
|
85
|
+
levels deep as no manifest, FAA-194 (27)); a folder entry the kit cannot
|
|
81
86
|
read is refused as `Cannot read <path>`, and a link, FIFO or other non-regular file is never followed. With a problem,
|
|
82
87
|
`--apply` writes nothing. Repacking drops old signatures and
|
|
83
88
|
says so: a changed package is signed again.
|
|
@@ -88,6 +88,7 @@ export class Responder extends SlotKeeper {
|
|
|
88
88
|
this.waitUntil = 0;
|
|
89
89
|
this.stopping = false;
|
|
90
90
|
this.claimKeys = {};
|
|
91
|
+
this.probing = new Set(); // moved slots whose move-back claim lost its answer: asked again under the same key
|
|
91
92
|
this.said = {};
|
|
92
93
|
this.rr = 0;
|
|
93
94
|
this.reconcileAt = 0;
|
|
@@ -172,6 +173,7 @@ export class Responder extends SlotKeeper {
|
|
|
172
173
|
this.state = 'connecting';
|
|
173
174
|
this.slots = {}; // the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
|
|
174
175
|
this.confirmed = {}; // nor its confirmations: a slot may have moved between the two sessions
|
|
176
|
+
this.probing = new Set(); // nor its lost move-back claims
|
|
175
177
|
const grant = await this.hub.joinBoard(this.project, this.credentialFile);
|
|
176
178
|
const missing = this.capabilities.filter((c) => !grant.capabilities.includes(c));
|
|
177
179
|
const session = this.sessionFactory(grant);
|
|
@@ -462,7 +464,9 @@ export class Responder extends SlotKeeper {
|
|
|
462
464
|
/** Seconds this session's leases still run, by its own clock: the running attempt's, else the oldest held slot's. */
|
|
463
465
|
leaseLeft() {
|
|
464
466
|
const renewed = this.current ? [this.current.renewedAt ?? 0]
|
|
465
|
-
: Object.values(this.slots).filter((s) => s.state === 'held' && s.renewedAt !== -Infinity).map((s) => s.renewedAt ?? 0)
|
|
467
|
+
: [...Object.values(this.slots).filter((s) => s.state === 'held' && s.renewedAt !== -Infinity).map((s) => s.renewedAt ?? 0), // a lapsed slot holds no lease (FAA-163)
|
|
468
|
+
...Object.entries(this.slots).filter(([c, s]) => this.probing.has(c) && 'probeAt' in s && this.now() - s.probeAt < LEASE_SECONDS - LEASE_MARGIN_SECONDS)
|
|
469
|
+
.map(([, s]) => s.probeAt)]; // a move-back claim whose answer was lost holds one from its send, while more than the 10 s margin of that lease is left (FAA-165)
|
|
466
470
|
return renewed.length && this.session ? Math.min(...renewed) + LEASE_SECONDS - this.now() : 0;
|
|
467
471
|
}
|
|
468
472
|
|
|
@@ -528,7 +532,10 @@ export class Responder extends SlotKeeper {
|
|
|
528
532
|
this.suspect.delete(capability);
|
|
529
533
|
}
|
|
530
534
|
for (const [capability, slot] of Object.entries(this.slots)) {
|
|
531
|
-
if (slot.state === 'moved')
|
|
535
|
+
if (slot.state === 'moved') { // never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
536
|
+
if (this.now() - (slot.renewedAt ?? 0) >= RENEW_SECONDS - 0.5) await this.movedBack(capability); // unless com.status shows a later generation it may hold (FAA-165)
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
532
539
|
if (slot.state === 'paused') {
|
|
533
540
|
// still held: a longer pause; lifted: the slot is ours again
|
|
534
541
|
if (capability !== heldByWork && this.now() >= (slot.retryAt ?? 0) && !(await this.gone(capability))) await this.enroll(capability); // a move is read first
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -16,6 +16,8 @@ export const HOLD_RETRY_MAX_SECONDS = 900;
|
|
|
16
16
|
export const SESSION_CODES = Object.freeze(new Set(['unreachable', 'session-ended', 'not_authorized', 'authority_changed', 'credential', 'no-epoch',
|
|
17
17
|
'schema-unsupported', 'checkpoint', 'receipt-shape', 'idempotency_conflict', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol', 'not_available']));
|
|
18
18
|
|
|
19
|
+
const sameRecord = (a, b) => JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
|
|
20
|
+
|
|
19
21
|
/** The slot methods of Responder, which owns every field they read (checkpoint, slots, project, locked). */
|
|
20
22
|
export class SlotKeeper {
|
|
21
23
|
// -- a slot moved to another session (FAA-155, DEC-0140 PR-2) --
|
|
@@ -127,13 +129,88 @@ export class SlotKeeper {
|
|
|
127
129
|
this.toResume = this.toResume.filter((r) => this.checkpoint.entries[r]?.capability !== capability);
|
|
128
130
|
this.slots[capability] = { state: 'moved', generation: now, renewedAt: this.now() };
|
|
129
131
|
delete this.confirmed[capability];
|
|
132
|
+
delete this.claimKeys[capability]; // the move returned that claim to the queue: its receipt is dead
|
|
130
133
|
if (!('moved' in held)) { // once, even across restarts
|
|
131
|
-
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it
|
|
134
|
+
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it; accepted work stays \`accepted\` (work begun is the new holder's). The operator brings it back by take-back (once the other session lets it go, or at a restart with accepted work waiting; a restart alone does not) or, where Fabric offers it, a move back here. Else ask its maintainer.`, { notify: true, episode: capability, capability, generation: now, held: held.generation });
|
|
132
135
|
this.note(capability, { ...held, moved: now }); // said again at the next start if it cannot be written
|
|
133
136
|
}
|
|
134
137
|
return true;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
|
|
142
|
+
* (DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
|
|
143
|
+
* com.claim asks whether the slot is its own. A claim that lands makes it held again and starts what it handed out; a
|
|
144
|
+
* lease_expired one (the move reached this session but its lease lapsed) enrolls again; a fenced one is another
|
|
145
|
+
* session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a slot another
|
|
146
|
+
* session holds: a slot moved off stays off unless it is handed back.
|
|
147
|
+
*/
|
|
148
|
+
async movedBack(capability) {
|
|
149
|
+
if (this.current || this.stopping) return; // a claim now could hand out a second request, or start work while the responder stops
|
|
150
|
+
const held = this.checkpoint.slots[capability] ?? {};
|
|
151
|
+
if (held.project !== this.project || !isInt(held.moved, 1)) return;
|
|
152
|
+
if (this.slots[capability]) this.slots[capability].renewedAt = this.now();
|
|
153
|
+
const now = ((await this.status()).responders ?? []).find((r) => isObj(r) && r.capability === capability)?.generation;
|
|
154
|
+
if (!isInt(now, 1) || now <= held.moved) return;
|
|
155
|
+
if (!this.probing.has(capability) || !this.claimKeys[capability]) { // a fresh key: one from before the move may hold a dead receipt
|
|
156
|
+
this.claimKeys[capability] = this.key('claim');
|
|
157
|
+
if (this.slots[capability]) delete this.slots[capability].probeAt; // and a send time of its own
|
|
158
|
+
}
|
|
159
|
+
let answer;
|
|
160
|
+
const sent = this.now();
|
|
161
|
+
try {
|
|
162
|
+
answer = this.receipt('com.claim', await this.call('com.claim', { idempotency: this.idem(this.claimKeys[capability]), capability }));
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (!(e instanceof BoardError)) throw e; // not the board's answer
|
|
165
|
+
if (e.code === 'unreachable') { // the claim may have landed: asked again under this key, never left to the fault
|
|
166
|
+
this.probing.add(capability);
|
|
167
|
+
if (this.slots[capability] && !('probeAt' in this.slots[capability])) this.slots[capability].probeAt = sent; // its lease runs from the first send
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.probing.delete(capability);
|
|
171
|
+
if (e.code === 'held') return; // the operator's hold: asked again at the next renewal
|
|
172
|
+
if (e.code === 'idempotency_window_expired' || e.code === 'capacity_exceeded') {
|
|
173
|
+
delete this.claimKeys[capability];
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (e.code === 'lease_expired') {
|
|
177
|
+
delete this.claimKeys[capability]; // a dead attempt's receipt may sit under it
|
|
178
|
+
if (await this.back(capability, held, now)) {
|
|
179
|
+
this.slots[capability].renewedAt = -Infinity; // lapsed: the enroll below is recorded first
|
|
180
|
+
await this.enroll(capability);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (SESSION_CODES.has(e.code) && e.code !== 'not_authorized') throw e; // the board session's fault, not this slot's
|
|
185
|
+
// fenced, or another refusal of this claim: not this session's slot at this generation; a takeBack meanwhile wins (FAA-158)
|
|
186
|
+
await this.locked(() => { if (sameRecord(this.checkpoint.slots[capability], held)) this.note(capability, { ...held, moved: now }); });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.probing.delete(capability);
|
|
190
|
+
const generation = isObj(answer.attempt) && isInt(answer.attempt.generation, 1) ? answer.attempt.generation : now;
|
|
191
|
+
if (generation < now) { // a replay from before a later move: dead, asked again fresh
|
|
192
|
+
delete this.claimKeys[capability];
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!(await this.back(capability, held, generation))) return;
|
|
196
|
+
if ('request' in answer) {
|
|
197
|
+
delete this.claimKeys[capability];
|
|
198
|
+
this.slots[capability].renewedAt = this.now(); // the claim just landed: its lease runs (finding 38)
|
|
199
|
+
await this.start(capability, answer);
|
|
200
|
+
} else await this.enroll(capability); // renewed now: whether an empty com.claim renews the lease is not the contract's to say
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The slot is this session's again at `generation`: held, its record rewritten without the move (FAA-165). */
|
|
204
|
+
back(capability, held, generation) {
|
|
205
|
+
return this.locked(() => {
|
|
206
|
+
if (!sameRecord(this.checkpoint.slots[capability], held)) return false; // a takeBack forgot it; its own next tick enrolls
|
|
207
|
+
this.slots[capability] = { state: 'held', generation, renewedAt: -Infinity }; // no lease yet
|
|
208
|
+
this.heldAt(capability, generation);
|
|
209
|
+
delete this.said['board.slot.moved']; // a later move is said again
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
137
214
|
/**
|
|
138
215
|
* The operator's act, never the kit's: forget that a capability's slot moved away. In the same board session the next
|
|
139
216
|
* tick enrolls once the slot is free; a reconnect with held work takes it back at once by a transfer (FAA-155).
|
|
@@ -141,6 +218,8 @@ export class SlotKeeper {
|
|
|
141
218
|
takeBack(capability) {
|
|
142
219
|
if (!this.capabilities.includes(capability)) throw new BoardError('invalid_arguments', `This responder does not answer ${capability}.`);
|
|
143
220
|
this.checkpoint.slot(capability, null);
|
|
221
|
+
delete this.claimKeys[capability]; // a receipt from before the take-back may be dead
|
|
222
|
+
this.probing.delete(capability);
|
|
144
223
|
if (this.slots[capability]?.state === 'moved') this.slots[capability] = { state: 'contested', renewedAt: 0 };
|
|
145
224
|
delete this.said['board.slot.moved'];
|
|
146
225
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs
CHANGED
|
@@ -504,12 +504,17 @@ export function compose(draft, files) {
|
|
|
504
504
|
return { pkg, notes };
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
/** What a folder problem means, where its code alone does not say what to change (FAA-193). */
|
|
508
|
+
const FOLDER_MESSAGES = {
|
|
509
|
+
'provider-manifest': 'provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets fabric.package.json must carry the same costPresets (copy them; pack does not)',
|
|
510
|
+
};
|
|
511
|
+
|
|
507
512
|
function allProblems(pkg, files) {
|
|
508
513
|
return [
|
|
509
514
|
...schemaProblems(pkg).map((p) => ({ ...p, rule: 'schema' })),
|
|
510
515
|
...pathProblems(pkg).map((p) => ({ ...p, rule: 'path' })),
|
|
511
516
|
...packageFindings(pkg).map((p) => ({ ...p, rule: p.code })),
|
|
512
|
-
...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: p.problem })),
|
|
517
|
+
...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: FOLDER_MESSAGES[p.problem] ?? p.problem })),
|
|
513
518
|
];
|
|
514
519
|
}
|
|
515
520
|
|