@passioncode-ai/fabric-agent-adapter 0.35.3 → 0.36.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 +48 -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 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +45 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +37 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +13 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +129 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +67 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +252 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +5 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +109 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +194 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +82 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +356 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +10 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/share-registration.schema.json +218 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +13 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +3 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +20 -5
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://fabric.passioncode.ai/agent-contract/0.1.0/schemas/share-registration.schema.json",
|
|
4
|
+
"title": "Share registration, share-registration/1, and its receipt (DEC-0086)",
|
|
5
|
+
"description": "How an organization server learns which agent a share grant into it runs: the sharer's enrolled node registers each such grant over the device transport (devices.md, the DEC-0056 signature by the enrollment's #sig key), and the server answers a receipt or a typed refusal. The server takes the organization from the certificate and the registrant from the user bound to its epoch, never from the body, and keys what it holds by (that user, grantId). docs/specification/share-registration.md.",
|
|
6
|
+
"oneOf": [
|
|
7
|
+
{
|
|
8
|
+
"$ref": "#/$defs/registration"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"$ref": "#/$defs/receipt"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"$ref": "#/$defs/refusal"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"$defs": {
|
|
18
|
+
"registration": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "What the sharer's enrolled node sends to the organization server of one of its enrollments for one share grant into that organization (DEC-0086 items 1 to 3): active, with the grant's revision, the agentRef its placements run and the grant's window; or withdrawn, with the time it ended. Nothing else: no estate id or revision, machine, principal, issuer, project, capability, spend or free text.",
|
|
21
|
+
"required": [
|
|
22
|
+
"type",
|
|
23
|
+
"grantId",
|
|
24
|
+
"context",
|
|
25
|
+
"state"
|
|
26
|
+
],
|
|
27
|
+
"properties": {
|
|
28
|
+
"type": {
|
|
29
|
+
"const": "share-registration/1"
|
|
30
|
+
},
|
|
31
|
+
"grantId": {
|
|
32
|
+
"$ref": "#/$defs/shareGrantId",
|
|
33
|
+
"description": "The share grant's meta.id, compared as written (FAC-SEM-117)."
|
|
34
|
+
},
|
|
35
|
+
"context": {
|
|
36
|
+
"$ref": "#/$defs/orgContextId",
|
|
37
|
+
"description": "The grant's own context, org:<orgId>: the receiver refuses one that is not org: and its certificate's org.id as written (wrong-organization)."
|
|
38
|
+
},
|
|
39
|
+
"state": {
|
|
40
|
+
"enum": [
|
|
41
|
+
"active",
|
|
42
|
+
"withdrawn"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"grantRevision": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 1,
|
|
48
|
+
"maximum": 9007199254740991,
|
|
49
|
+
"description": "The grant's meta.revision in the estate revision the node applied. A higher one replaces the held registration; a lower one is stale-revision; the same one with other bytes revision-conflict."
|
|
50
|
+
},
|
|
51
|
+
"agentRef": {
|
|
52
|
+
"$ref": "#/$defs/agentRef",
|
|
53
|
+
"description": "The one agentRef every placement of the revision in this context that names the grant runs (FAC-SEM-117)."
|
|
54
|
+
},
|
|
55
|
+
"startsAt": {
|
|
56
|
+
"$ref": "#/$defs/instant",
|
|
57
|
+
"description": "The grant's startsAt, as written."
|
|
58
|
+
},
|
|
59
|
+
"expiresAt": {
|
|
60
|
+
"$ref": "#/$defs/instant",
|
|
61
|
+
"description": "The grant's expiresAt, as written; after startsAt (FAC-SEM-118)."
|
|
62
|
+
},
|
|
63
|
+
"withdrawnAt": {
|
|
64
|
+
"$ref": "#/$defs/instant",
|
|
65
|
+
"description": "When the share ended: the earliest revokedAt of the grant's revocations in the revision or the applied delta entries, or the issuedAt of the revision that no longer lists the grant."
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"allOf": [
|
|
69
|
+
{
|
|
70
|
+
"if": {
|
|
71
|
+
"properties": {
|
|
72
|
+
"state": {
|
|
73
|
+
"const": "active"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": [
|
|
77
|
+
"state"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"then": {
|
|
81
|
+
"required": [
|
|
82
|
+
"grantRevision",
|
|
83
|
+
"agentRef",
|
|
84
|
+
"startsAt",
|
|
85
|
+
"expiresAt"
|
|
86
|
+
],
|
|
87
|
+
"properties": {
|
|
88
|
+
"grantRevision": true,
|
|
89
|
+
"agentRef": true,
|
|
90
|
+
"startsAt": true,
|
|
91
|
+
"expiresAt": true,
|
|
92
|
+
"withdrawnAt": false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"if": {
|
|
98
|
+
"properties": {
|
|
99
|
+
"state": {
|
|
100
|
+
"const": "withdrawn"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": [
|
|
104
|
+
"state"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"then": {
|
|
108
|
+
"required": [
|
|
109
|
+
"withdrawnAt"
|
|
110
|
+
],
|
|
111
|
+
"properties": {
|
|
112
|
+
"withdrawnAt": true,
|
|
113
|
+
"grantRevision": false,
|
|
114
|
+
"agentRef": false,
|
|
115
|
+
"startsAt": false,
|
|
116
|
+
"expiresAt": false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"additionalProperties": false
|
|
122
|
+
},
|
|
123
|
+
"receipt": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"description": "The receiver's 200 answer: what it holds now for the device's bound user and the grant.",
|
|
126
|
+
"required": [
|
|
127
|
+
"type",
|
|
128
|
+
"grantId",
|
|
129
|
+
"state",
|
|
130
|
+
"digest"
|
|
131
|
+
],
|
|
132
|
+
"properties": {
|
|
133
|
+
"type": {
|
|
134
|
+
"const": "share-registration-receipt/1"
|
|
135
|
+
},
|
|
136
|
+
"grantId": {
|
|
137
|
+
"$ref": "#/$defs/shareGrantId"
|
|
138
|
+
},
|
|
139
|
+
"state": {
|
|
140
|
+
"enum": [
|
|
141
|
+
"registered",
|
|
142
|
+
"unchanged",
|
|
143
|
+
"withdrawn"
|
|
144
|
+
],
|
|
145
|
+
"description": "registered: an active registration was taken; withdrawn: a withdrawal was taken; unchanged: the same bytes again, or a second withdrawal (of two, the earliest withdrawnAt is kept)."
|
|
146
|
+
},
|
|
147
|
+
"digest": {
|
|
148
|
+
"$ref": "#/$defs/sha256",
|
|
149
|
+
"description": "sha256: and the hex SHA-256 of the UTF-8 RFC 8785 form of the registration the receiver holds after this request (shareRegistrationDigest)."
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"additionalProperties": false
|
|
153
|
+
},
|
|
154
|
+
"refusal": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"description": "The receiver's refusal of a registration after the request authenticated (devices.md transport steps 0 to 8) and its revision is one it reads: 400 malformed, 403 wrong-organization or unassigned-device, 409 already-withdrawn, agent-mismatch, stale-revision or revision-conflict.",
|
|
157
|
+
"required": [
|
|
158
|
+
"error"
|
|
159
|
+
],
|
|
160
|
+
"properties": {
|
|
161
|
+
"error": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"required": [
|
|
164
|
+
"code",
|
|
165
|
+
"message"
|
|
166
|
+
],
|
|
167
|
+
"properties": {
|
|
168
|
+
"code": {
|
|
169
|
+
"enum": [
|
|
170
|
+
"malformed",
|
|
171
|
+
"wrong-organization",
|
|
172
|
+
"unassigned-device",
|
|
173
|
+
"already-withdrawn",
|
|
174
|
+
"agent-mismatch",
|
|
175
|
+
"stale-revision",
|
|
176
|
+
"revision-conflict"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"message": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"minLength": 1,
|
|
182
|
+
"maxLength": 200
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"additionalProperties": false
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"additionalProperties": false
|
|
189
|
+
},
|
|
190
|
+
"instant": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"format": "date-time",
|
|
193
|
+
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](\\.[0-9]{1,3})?([Zz]|[+-][0-9]{2}:[0-9]{2})$",
|
|
194
|
+
"description": "An RFC 3339 date-time with an offset, seconds 00 to 59 (no leap second, which date-time admits) and at most three fraction digits: every port compares the same instants, at millisecond precision (DEC-0085, DEC-0086)."
|
|
195
|
+
},
|
|
196
|
+
"orgContextId": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"pattern": "^org:[A-Za-z0-9][A-Za-z0-9_-]{0,127}$",
|
|
199
|
+
"description": "DEC-0086: an organization's context, org:<orgId>: contextId without personal. What a share registration and a grant.usage carry, the grant's own context. Compared as written (docs/specification/contexts.md)."
|
|
200
|
+
},
|
|
201
|
+
"sha256": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"pattern": "^sha256:[a-f0-9]{64}$"
|
|
204
|
+
},
|
|
205
|
+
"shareGrantId": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"format": "uri",
|
|
208
|
+
"pattern": "^urn:fabric:share:(?!0{32}$)[0-9a-f]{32}$",
|
|
209
|
+
"description": "DEC-0086, FAC-SEM-117: the meta.id of a share grant into an organization: urn:fabric:share: and 32 lower-case hex digits, 128 bits from a CSPRNG, never derived from a name, an address, an estate id, an agentRef or the organization, and never all zeros. The pattern admits only its own normal form (DEC-0076), so it is compared as written; it holds no person's name, address or estate id (docs/specification/share-registration.md)."
|
|
210
|
+
},
|
|
211
|
+
"agentRef": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"maxLength": 146,
|
|
214
|
+
"pattern": "^urn:fabric:agent:[a-z][a-z0-9]*(?:-[a-z0-9]+)*:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
215
|
+
"description": "urn:fabric:agent:<publisher>:<id> — one agent across its versions (FAC-SEM-066)."
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -5,9 +5,9 @@ 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. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.36.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "07042181bfae2a481aa0da1a5651f24c76669c48"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -124,7 +124,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
124
124
|
The effect class is declared, never defaulted: ask the author what the agent does outside itself when the request
|
|
125
125
|
does not say. Cost presets are only for a run whose cost varies; their amounts come from the author or measured runs.
|
|
126
126
|
|
|
127
|
-
Pin exactly contract `0.1.0` at commit `
|
|
127
|
+
Pin exactly contract `0.1.0` at commit `07042181bfae2a481aa0da1a5651f24c76669c48` and
|
|
128
128
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
129
129
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
130
130
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|
|
@@ -19,9 +19,14 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
|
|
|
19
19
|
|
|
20
20
|
## Limits are pauses
|
|
21
21
|
|
|
22
|
-
1. **A provider or subscription limit pauses the job; it never fails it.**
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
1. **A provider or subscription limit pauses the job; it never fails it.** Keep the job `working` and
|
|
23
|
+
write its `statusMessage` in the contract's pause grammar, `quota-exhausted` or
|
|
24
|
+
`quota-exhausted until <UTC instant>` (optionally `: <text>`, at most 500 characters), with
|
|
25
|
+
`pollIntervalMs` reaching the reset; resume from the same stage. Only a job its provider will not
|
|
26
|
+
resume ends `failed` with `error: {code: "quota-exhausted", message, data: {retryable, resetsAt?}}`:
|
|
27
|
+
`retryable: false` when a person must act, and `resetsAt` (UTC, `T` and `Z`, at most three
|
|
28
|
+
fraction digits) only when `retryable: true` (contract DEC-0087, `FAC-SEM-119`). Failing a job a
|
|
29
|
+
limit only paused cost callers whole jobs and resubmits.
|
|
25
30
|
2. **Resume first.** Every worker reads its own state — checkpoint, worktree, branches, open
|
|
26
31
|
pull requests, notes — before it acts, and continues from there. Relaunching from scratch
|
|
27
32
|
after a limit recreated work over committed work and re-issued credentials already held.
|
|
@@ -88,7 +93,11 @@ mechanism that held after the fix. Every rule is cheaper to build in than to ret
|
|
|
88
93
|
4. **A resend of an unknown outcome reuses its idempotency key — or does not happen.** Derive
|
|
89
94
|
keys from the intent (action and entity, or run and step), never from a clock or a random
|
|
90
95
|
value, and make them unique in storage: time-and-random keys let redelivered requests charge
|
|
91
|
-
again. A write whose effect is uncertain
|
|
96
|
+
again. A write whose effect is uncertain ends `failed` with the code `outcome-unknown`; only an
|
|
97
|
+
observed result settles it, and a new key there pays twice. A caller acts on each answer as the
|
|
98
|
+
contract's `jobCallerAction` does: wait out a pause, resend a retryable `quota-exhausted` under the
|
|
99
|
+
same key after `resetsAt`, hand a non-retryable one or `limit-reached` to a person, reconcile an
|
|
100
|
+
`outcome-unknown`, and never resend it.
|
|
92
101
|
5. **A client error at submit is a rejection, not a lost task.** Settle it with known usage (zero
|
|
93
102
|
if none), release the reserved money in the same transaction, and never resend the identical
|
|
94
103
|
request; a 4xx while polling is still a lost task.
|
|
@@ -87,6 +87,9 @@ only variable names are looked at, never a value (two of them carry the session'
|
|
|
87
87
|
- **Start a second Fabric session.** A `fabric-mcp` started on its own would enroll a new session, and Fabric ends a
|
|
88
88
|
binding's oldest live session past eight (ADR-0134 amendment 2) — the agent's own. The worker and `watch-mail` run
|
|
89
89
|
`fabric-mcp --rejoin-only --runtime-session-id <id>`, and only when Fabric advertises `session-hooks/1`.
|
|
90
|
+
- **Rejoin more than once a minute.** Every rejoin spends the agent's Fabric request budget, which every session of
|
|
91
|
+
the machine shares. A turn within 60 s of the session's last successful rejoin (the worker marks it as `rejoined`
|
|
92
|
+
in the session's folder) is spooled and waits for the next worker; the end of a session always starts one (FAA-53).
|
|
90
93
|
- **Retry in a loop.** A refused call is logged by its error code and dropped; an answer that never came is logged
|
|
91
94
|
as `outcome=unknown`. The calls carry an `idempotencyKey`, so a repeat is the same event.
|
|
92
95
|
|
|
@@ -56,6 +56,9 @@ LOCK_STALE_S = 30.0
|
|
|
56
56
|
#: A session folder untouched this long belongs to a session that will not spool again: its worker failed, or the
|
|
57
57
|
#: session ended without SessionEnd. Nothing retries it, so the next spooling hook removes it (FAA-50).
|
|
58
58
|
SESSION_STALE_S = 7 * 86400
|
|
59
|
+
#: A session rejoins at most once in this span (FAA-53): every rejoin spends Fabric's per-agent request budget, which
|
|
60
|
+
#: every session of the machine shares; a turn inside it waits in the spool for the next worker, an end never waits.
|
|
61
|
+
REJOIN_MIN_INTERVAL_S = 60.0
|
|
59
62
|
READ_CAP = 64 * 1024
|
|
60
63
|
CONFIG_CAP = 32 * 1024 * 1024
|
|
61
64
|
WATCH_MIN_INTERVAL_S = 10
|
|
@@ -63,7 +66,7 @@ WATCH_MAX_BACKOFF_S = 900
|
|
|
63
66
|
SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
|
|
64
67
|
LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
|
|
65
68
|
LOCAL_ORIGIN = re.compile(r'"origin": "http://(?:127\.0\.0\.1|localhost)')
|
|
66
|
-
VERSION = "0.
|
|
69
|
+
VERSION = "0.36.0"
|
|
67
70
|
HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
|
|
68
71
|
"hermes": "Hermes", "opencode": "OpenCode", "openclaw": "OpenClaw", "pi": "Pi"}
|
|
69
72
|
NATIVE_PLUGIN_HOSTS = frozenset(("hermes", "opencode", "openclaw", "pi"))
|
|
@@ -559,10 +562,21 @@ def hook_spool(payload: Dict[str, Any], event: str, env: Optional[Dict[str, str]
|
|
|
559
562
|
if event == "end" and isinstance(payload.get("reason"), str):
|
|
560
563
|
entry["reason"] = payload["reason"][:64]
|
|
561
564
|
spool(directory, entry)
|
|
562
|
-
if event
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
565
|
+
if event != "end" and lock_held(directory):
|
|
566
|
+
return "spooled: a worker is running"
|
|
567
|
+
if event != "end" and rejoined_recently(directory):
|
|
568
|
+
return "spooled: rejoined recently"
|
|
569
|
+
spawn(directory)
|
|
570
|
+
return "spooled: worker started"
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def rejoined_recently(directory: Path, now: Optional[float] = None) -> bool:
|
|
574
|
+
"""Whether this session's last rejoin succeeded within REJOIN_MIN_INTERVAL_S (the worker marks it)."""
|
|
575
|
+
try:
|
|
576
|
+
mark = (directory / "rejoined").stat().st_mtime
|
|
577
|
+
except OSError:
|
|
578
|
+
return False
|
|
579
|
+
return 0 <= (time.time() if now is None else now) - mark < REJOIN_MIN_INTERVAL_S
|
|
566
580
|
|
|
567
581
|
|
|
568
582
|
def _remove_tree(directory: Path) -> None:
|
|
@@ -801,6 +815,7 @@ def deliver(directory: Path, env: Optional[Dict[str, str]] = None, deadline_s: f
|
|
|
801
815
|
"clientInfo": {"name": "fabric-agent-adapter-hooks", "version": VERSION}})
|
|
802
816
|
client.notify("notifications/initialized")
|
|
803
817
|
names = _wait_for_session_tools(client, deadline)
|
|
818
|
+
_write_private(directory / "rejoined", "")
|
|
804
819
|
while entries:
|
|
805
820
|
for path in entries:
|
|
806
821
|
entry = json.loads(read_small(path) or "{}")
|