agents-can-communicate 0.1.18 → 0.2.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/README.md +78 -70
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +6 -1
- package/docs/ADAPTER_AUTHORING.md +172 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +102 -214
- package/docs/CLI.md +157 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +143 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +109 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +102 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +42 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +12 -4
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +18 -11
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +9 -9
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +11 -30
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +9 -2
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +11 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +115 -63
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +19 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -12,7 +12,7 @@ export const CONFIG_FILENAME = "acc.workspace.json";
|
|
|
12
12
|
// the wrong place for them, and a config carrying them is either a mistake or
|
|
13
13
|
// an attempt to hand a peer state it would otherwise have to earn.
|
|
14
14
|
export const RUNTIME_KEYS = Object.freeze(["sessions", "participants", "messages",
|
|
15
|
-
"claims", "receipts", "intents", "events", "tokens", "credentials"]);
|
|
15
|
+
"claims", "receipts", "intents", "events", "deliveryBindings", "tokens", "credentials"]);
|
|
16
16
|
|
|
17
17
|
const KNOWN_KEYS = Object.freeze(["schemaVersion", "workspaceId", "displayName",
|
|
18
18
|
"roots", "policy", "requiredAdapters", "extensions"]);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AccError, EXIT } from "./errors.mjs";
|
|
2
|
+
|
|
3
|
+
export const VALID_OBLIGATIONS = Object.freeze({
|
|
4
|
+
note: Object.freeze(["none"]),
|
|
5
|
+
question: Object.freeze(["reply"]),
|
|
6
|
+
request: Object.freeze(["reply"]),
|
|
7
|
+
answer: Object.freeze(["none"]),
|
|
8
|
+
decision: Object.freeze(["none", "acknowledge"]),
|
|
9
|
+
handoff: Object.freeze(["none", "acknowledge"]),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const MESSAGE_KINDS = Object.freeze(Object.keys(VALID_OBLIGATIONS));
|
|
13
|
+
// The generic send surface cannot construct reply links or structured handoff
|
|
14
|
+
// payloads. Those two complete shapes belong to reply and finish respectively.
|
|
15
|
+
export const GENERIC_MESSAGE_KINDS = Object.freeze(
|
|
16
|
+
["note", "question", "request", "decision"]);
|
|
17
|
+
export const OBLIGATIONS = Object.freeze(["none", "acknowledge", "reply"]);
|
|
18
|
+
|
|
19
|
+
const data = (message, details) => {
|
|
20
|
+
throw new AccError(EXIT.DATA, message, details);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function assertMessageSemantics(message) {
|
|
24
|
+
const room = message.toParticipantIds.length === 0;
|
|
25
|
+
if (room && !["note", "decision", "handoff"].includes(message.kind)) {
|
|
26
|
+
data(`a room message cannot have kind ${message.kind}`, { kind: message.kind });
|
|
27
|
+
}
|
|
28
|
+
if (room && message.obligation !== "none") {
|
|
29
|
+
data("a room message must have obligation none", { obligation: message.obligation });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const valid = VALID_OBLIGATIONS[message.kind] ?? [];
|
|
33
|
+
if (!valid.includes(message.obligation)) {
|
|
34
|
+
data(`message obligation ${message.obligation} is invalid for ${message.kind}`,
|
|
35
|
+
{ kind: message.kind, obligation: message.obligation });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const root = message.threadId === message.messageId;
|
|
39
|
+
if (message.kind === "answer" && message.inReplyTo === null) {
|
|
40
|
+
data("an answer requires inReplyTo", { messageId: message.messageId });
|
|
41
|
+
}
|
|
42
|
+
if (message.kind === "answer" && root) {
|
|
43
|
+
data("an answer cannot be a thread root", { messageId: message.messageId });
|
|
44
|
+
}
|
|
45
|
+
if (message.inReplyTo === null && !root) {
|
|
46
|
+
data("a thread root requires threadId to equal messageId",
|
|
47
|
+
{ threadId: message.threadId, messageId: message.messageId });
|
|
48
|
+
}
|
|
49
|
+
if (message.inReplyTo !== null && root) {
|
|
50
|
+
data("a thread root must have inReplyTo null", { inReplyTo: message.inReplyTo });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (message.kind === "handoff" && message.handoff === null) {
|
|
54
|
+
data("a handoff message requires a handoff payload", { messageId: message.messageId });
|
|
55
|
+
}
|
|
56
|
+
if (!room && message.kind === "handoff" && message.obligation !== "acknowledge") {
|
|
57
|
+
data("an addressed handoff requires acknowledgement",
|
|
58
|
+
{ messageId: message.messageId, obligation: message.obligation });
|
|
59
|
+
}
|
|
60
|
+
if (message.kind !== "handoff" && message.handoff !== null) {
|
|
61
|
+
data(`a ${message.kind} message must have handoff null`, { kind: message.kind });
|
|
62
|
+
}
|
|
63
|
+
return message;
|
|
64
|
+
}
|
|
@@ -3,7 +3,10 @@ export { AccError, EXIT, isAccError } from "./errors.mjs";
|
|
|
3
3
|
export { assertPortableId, createId } from "./ids.mjs";
|
|
4
4
|
export { ENVELOPE_VERSION, failure, ok } from "./envelopes.mjs";
|
|
5
5
|
export { RECORD_KINDS, SCHEMA_VERSION, validateRecord } from "./schema.mjs";
|
|
6
|
+
export { GENERIC_MESSAGE_KINDS, MESSAGE_KINDS, OBLIGATIONS, VALID_OBLIGATIONS,
|
|
7
|
+
assertMessageSemantics }
|
|
8
|
+
from "./conversations.mjs";
|
|
6
9
|
export { CONFIG_FILENAME, CONFIG_SCHEMA_VERSION, RUNTIME_KEYS, defaultProjectConfig,
|
|
7
10
|
validateProjectConfig } from "./config.mjs";
|
|
8
|
-
export {
|
|
11
|
+
export { RECEIPT_STATES, advanceReceipt } from "./states.mjs";
|
|
9
12
|
export { assertMatchableResource, normaliseResource } from "./resources.mjs";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { assertMessageSemantics, MESSAGE_KINDS, OBLIGATIONS } from "./conversations.mjs";
|
|
1
2
|
import { AccError, EXIT } from "./errors.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { id, invalid, listOf, nullable, oneOf, plainObject, positiveInteger,
|
|
3
4
|
resourceUri, sequence, text, timestamp } from "./fields.mjs";
|
|
4
5
|
|
|
5
|
-
export const SCHEMA_VERSION =
|
|
6
|
+
export const SCHEMA_VERSION = 3;
|
|
6
7
|
|
|
7
8
|
const line = text();
|
|
8
9
|
const prose = text({ max: 4000, multiline: true });
|
|
@@ -14,10 +15,22 @@ const artifactKind = oneOf("file", "git", "url", "report", "image", "data");
|
|
|
14
15
|
/** @typedef {{ cursor: string, events: AccEvent[] }} EventPage */
|
|
15
16
|
/** @typedef {{ kind: string, priority: number, sourceId: string, summary: string }} AttentionItem */
|
|
16
17
|
/** @typedef {{ workspace: object, participants: object[], sessions: object[],
|
|
17
|
-
* intents: object[],
|
|
18
|
+
* intents: object[], claims: object[], messages: object[], receipts: object[] }} WorkspaceSnapshot */
|
|
19
|
+
|
|
20
|
+
function closedObject(value, field, fields) {
|
|
21
|
+
plainObject(value, field);
|
|
22
|
+
for (const key of Object.keys(value)) {
|
|
23
|
+
if (!Object.hasOwn(fields, key)) invalid(`${field}.${key}`, "is not a known field", value[key]);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, check] of Object.entries(fields)) {
|
|
26
|
+
if (!Object.hasOwn(value, key)) {
|
|
27
|
+
throw new AccError(EXIT.DATA, `${field} requires ${key}`, { field, key });
|
|
28
|
+
}
|
|
29
|
+
check(value[key], `${field}.${key}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
18
33
|
|
|
19
|
-
// Field names stay mappable to the A2A Agent Card, Task, Message, and Artifact
|
|
20
|
-
// concepts (spec section 11) without importing any A2A transport.
|
|
21
34
|
const artifactRef = (value, field) => {
|
|
22
35
|
plainObject(value, field);
|
|
23
36
|
const known = new Set(["kind", "uri", "description", "sha256"]);
|
|
@@ -33,120 +46,70 @@ const artifactRef = (value, field) => {
|
|
|
33
46
|
return value;
|
|
34
47
|
};
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
const handoffPayload = (value, field) => closedObject(value, field, {
|
|
50
|
+
status: oneOf("complete", "partial", "blocked"),
|
|
51
|
+
completed: listOf(line),
|
|
52
|
+
remaining: listOf(line),
|
|
53
|
+
blockers: listOf(line),
|
|
54
|
+
verification: listOf(artifactRef),
|
|
55
|
+
});
|
|
56
|
+
|
|
41
57
|
const EVENT_TYPES = Object.freeze([
|
|
42
58
|
"workspace.materialised",
|
|
43
59
|
"session.opened", "session.closed",
|
|
44
60
|
"intent.published", "intent.cleared",
|
|
45
|
-
"workstream.created", "workstream.coordinator_acquired",
|
|
46
|
-
"workstream.coordinator_released",
|
|
47
|
-
"task.created", "task.claimed", "task.transitioned", "task.unblocked",
|
|
48
|
-
"task.declined", "task.released",
|
|
49
61
|
"claim.acquired", "claim.released", "claim.renewed", "claim.force_released",
|
|
50
|
-
"message.
|
|
51
|
-
|
|
52
|
-
// so the set has to carry each one they can produce.
|
|
53
|
-
...["recorded", "queued", "injected", "seen", "acknowledged", "failed"]
|
|
54
|
-
.map(state => `message.${state}`),
|
|
55
|
-
...["accepted", "declined", "review", "done", "released"]
|
|
56
|
-
.map(outcome => `work.${outcome}`),
|
|
57
|
-
"work.requested",
|
|
62
|
+
"message.recorded", "message.offered", "message.retrieved", "message.acknowledged",
|
|
63
|
+
"message.offer_succeeded", "message.offer_failed",
|
|
58
64
|
]);
|
|
59
65
|
|
|
60
66
|
const eventType = oneOf(...EVENT_TYPES);
|
|
67
|
+
const receiptState = oneOf("queued", "offered", "retrieved", "acknowledged");
|
|
61
68
|
|
|
62
|
-
const
|
|
69
|
+
const DURABLE_RECORDS = Object.freeze({
|
|
63
70
|
workspace: { workspaceId: id, displayName: line, source: oneOf("config", "git", "directory"),
|
|
64
71
|
roots: listOf(line), createdAt: timestamp },
|
|
65
72
|
|
|
66
73
|
participant: { participantId: id, workspaceId: id, displayName: line,
|
|
67
74
|
kind: oneOf("agent", "human"), createdAt: timestamp },
|
|
68
75
|
|
|
69
|
-
// `enforcement` and `lifecycle` are what this session's harness can actually
|
|
70
|
-
// do, declared at attach. The harness name does not imply them: the same
|
|
71
|
-
// client guards or does not depending on its model and its approval mode, and
|
|
72
|
-
// a peer deciding whether to rely on a claim needs the answer, not the brand.
|
|
73
|
-
// A workspace spans every worktree of one repository, so the workspace id
|
|
74
|
-
// cannot say which checkout a session is sitting in. Recorded at attach from
|
|
75
|
-
// what discovery already resolved: without it nobody can tell which worktrees
|
|
76
|
-
// have an owner, and asking cannot answer for the agents that are not running
|
|
77
|
-
// - which are exactly the ones a clean-up is looking for.
|
|
78
76
|
session: { sessionId: id, participantId: id, workspaceId: id, generation: id,
|
|
79
77
|
harness: line, state: oneOf("open", "closed"), parentSessionId: nullable(id),
|
|
80
|
-
checkoutRoot: nullable(line), branch: nullable(line),
|
|
81
|
-
// The process behind this session, when it can be named. Null means nobody
|
|
82
|
-
// knows - no process table, or an ancestry that did not resolve - and is
|
|
83
|
-
// read as "judge this one by age alone", never as "dead".
|
|
84
|
-
pid: nullable(positiveInteger),
|
|
78
|
+
checkoutRoot: nullable(line), branch: nullable(line), pid: nullable(positiveInteger),
|
|
85
79
|
enforcement: oneOf("guarded", "advisory"), lifecycle: oneOf("managed", "manual"),
|
|
86
80
|
heartbeatCadenceMs: positiveInteger, startedAt: timestamp, heartbeatAt: timestamp },
|
|
87
81
|
|
|
88
82
|
intent: { sessionId: id, workspaceId: id, summary,
|
|
89
83
|
mode: oneOf("observe", "explore", "edit", "review", "coordinate", "wait"),
|
|
90
|
-
resourceHints: listOf(resourceUri),
|
|
84
|
+
resourceHints: listOf(resourceUri),
|
|
91
85
|
state: oneOf("active", "blocked", "waiting", "done"), updatedAt: timestamp },
|
|
92
86
|
|
|
93
|
-
workstream: { workstreamId: id, workspaceId: id, title: line, objective: prose,
|
|
94
|
-
coordinatorSessionId: nullable(id),
|
|
95
|
-
state: oneOf("open", "paused", "complete", "cancelled"), createdAt: timestamp },
|
|
96
|
-
|
|
97
|
-
// Two assignees, deliberately. `assigneeParticipantId` is who the work is
|
|
98
|
-
// for and survives that agent restarting; `assigneeSessionId` is the exact
|
|
99
|
-
// session doing it right now and dies with the process. Asking one field to
|
|
100
|
-
// be both would either lose the request when a terminal closes or claim a
|
|
101
|
-
// dead session is still working.
|
|
102
|
-
task: { taskId: id, workstreamId: nullable(id), workspaceId: id, title: line,
|
|
103
|
-
state: oneOf("pending", "in_progress", "review", "done", "blocked"),
|
|
104
|
-
assigneeParticipantId: nullable(id), assigneeSessionId: nullable(id),
|
|
105
|
-
// Who asked. Without it nothing could tell the requester that their work
|
|
106
|
-
// was accepted, declined or finished - the task knew who it was for and
|
|
107
|
-
// had no idea who was waiting on it.
|
|
108
|
-
requestedByParticipantId: nullable(id),
|
|
109
|
-
dependsOn: listOf(id), acceptance: listOf(line), detail: nullable(prose),
|
|
110
|
-
createdAt: timestamp },
|
|
111
|
-
|
|
112
87
|
claim: { claimId: id, workspaceId: id, ownerSessionId: id, resource: resourceUri,
|
|
113
88
|
mode: oneOf("shared", "exclusive"), enforcement: oneOf("advisory", "guarded"),
|
|
114
89
|
reason: line, acquiredAt: timestamp, expiresAt: timestamp, generation: id },
|
|
115
90
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fromParticipantId: id, toParticipantIds: listOf(id),
|
|
122
|
-
type: oneOf("note", "question", "answer", "contract_request", "contract_response",
|
|
123
|
-
"decision_proposal", "decision_result", "blocker", "review_request",
|
|
124
|
-
"review_result", "handoff", "work_request", "work_response"),
|
|
125
|
-
subject: line, body: prose, priority: oneOf("low", "normal", "high", "urgent"),
|
|
126
|
-
workstreamId: nullable(id), taskId: nullable(id), inReplyTo: nullable(id),
|
|
127
|
-
requiresAck: flag, artifacts: listOf(artifactRef), sentAt: timestamp },
|
|
91
|
+
message: { messageId: id, threadId: id, clientMessageId: id, workspaceId: id,
|
|
92
|
+
fromParticipantId: id, fromSessionId: id, toParticipantIds: listOf(id),
|
|
93
|
+
kind: oneOf(...MESSAGE_KINDS), obligation: oneOf(...OBLIGATIONS),
|
|
94
|
+
subject: line, body: prose, inReplyTo: nullable(id), artifacts: listOf(artifactRef),
|
|
95
|
+
handoff: nullable(handoffPayload), sentAt: timestamp },
|
|
128
96
|
|
|
129
97
|
receipt: { messageId: id, workspaceId: id, recipientParticipantId: id,
|
|
130
|
-
state:
|
|
131
|
-
updatedAt: timestamp },
|
|
132
|
-
|
|
133
|
-
decision: { decisionId: id, workspaceId: id, workstreamId: nullable(id), title: line,
|
|
134
|
-
outcome: prose, authority: oneOf("human", "workstream", "policy"),
|
|
135
|
-
decidedBy: listOf(id), evidence: listOf(artifactRef), supersedes: nullable(id),
|
|
136
|
-
decidedAt: timestamp },
|
|
137
|
-
|
|
138
|
-
artifact: { kind: artifactKind, uri: resourceUri, description: line },
|
|
139
|
-
|
|
140
|
-
handoff: { handoffId: id, workspaceId: id, fromSessionId: id, toParticipantId: nullable(id),
|
|
141
|
-
goal: line, status: oneOf("complete", "partial", "blocked"), completed: listOf(line),
|
|
142
|
-
remaining: listOf(line), blockers: listOf(line), claimsToRelease: listOf(resourceUri),
|
|
143
|
-
verification: listOf(artifactRef), artifacts: listOf(artifactRef), createdAt: timestamp },
|
|
98
|
+
state: receiptState, updatedAt: timestamp },
|
|
144
99
|
|
|
145
100
|
event: { sequence, eventId: id, workspaceId: id, actorSessionId: id, type: eventType,
|
|
146
101
|
occurredAt: timestamp, payload: plainObject },
|
|
147
102
|
});
|
|
148
103
|
|
|
149
|
-
|
|
104
|
+
const RECORDS = Object.freeze({
|
|
105
|
+
...DURABLE_RECORDS,
|
|
106
|
+
deliveryBinding: { sessionId: id, generation: id, adapterId: id, clientVersion: line,
|
|
107
|
+
availableModes: listOf(oneOf("nextTurn", "livePush", "replyRoute")),
|
|
108
|
+
livePolicy: oneOf("off", "actionable", "all"), opaqueEndpointRef: prose,
|
|
109
|
+
leaseUntil: timestamp },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const RECORD_KINDS = Object.freeze(Object.keys(DURABLE_RECORDS));
|
|
150
113
|
|
|
151
114
|
export function validateRecord(kind, value) {
|
|
152
115
|
const fields = RECORDS[kind];
|
|
@@ -161,9 +124,7 @@ export function validateRecord(kind, value) {
|
|
|
161
124
|
}
|
|
162
125
|
for (const key of Object.keys(value)) {
|
|
163
126
|
if (key === "schemaVersion" || key === "extensions") continue;
|
|
164
|
-
if (!Object.hasOwn(fields, key)) {
|
|
165
|
-
invalid(`${kind}.${key}`, "is not a known field", value[key]);
|
|
166
|
-
}
|
|
127
|
+
if (!Object.hasOwn(fields, key)) invalid(`${kind}.${key}`, "is not a known field", value[key]);
|
|
167
128
|
}
|
|
168
129
|
for (const [field, check] of Object.entries(fields)) {
|
|
169
130
|
if (!Object.hasOwn(value, field)) {
|
|
@@ -171,9 +132,7 @@ export function validateRecord(kind, value) {
|
|
|
171
132
|
}
|
|
172
133
|
check(value[field], field);
|
|
173
134
|
}
|
|
174
|
-
// Forward-compatible metadata is tolerated only inside a named container, so
|
|
175
|
-
// an older reader can round-trip a newer writer's record without guessing
|
|
176
|
-
// which unknown top-level keys are safe.
|
|
177
135
|
if (Object.hasOwn(value, "extensions")) plainObject(value.extensions, "extensions");
|
|
136
|
+
if (kind === "message") assertMessageSemantics(value);
|
|
178
137
|
return value;
|
|
179
138
|
}
|
|
@@ -1,55 +1,28 @@
|
|
|
1
1
|
import { AccError, EXIT } from "./errors.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// recipient's receipt can only move forwards, and never rewrites another
|
|
6
|
-
// recipient's state.
|
|
7
|
-
export const DELIVERY_STATES = Object.freeze(
|
|
8
|
-
["recorded", "queued", "injected", "seen", "acknowledged", "failed"]);
|
|
3
|
+
export const RECEIPT_STATES = Object.freeze(
|
|
4
|
+
["queued", "offered", "retrieved", "acknowledged"]);
|
|
9
5
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
seen: ["acknowledged"],
|
|
6
|
+
const RECEIPT_NEXT = Object.freeze({
|
|
7
|
+
queued: ["offered", "retrieved", "acknowledged"],
|
|
8
|
+
offered: ["retrieved", "acknowledged"],
|
|
9
|
+
retrieved: ["acknowledged"],
|
|
15
10
|
acknowledged: [],
|
|
16
|
-
failed: [],
|
|
17
11
|
});
|
|
18
12
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const TASK_NEXT = Object.freeze({
|
|
23
|
-
pending: ["in_progress", "blocked"],
|
|
24
|
-
in_progress: ["review", "done", "blocked"],
|
|
25
|
-
review: ["in_progress", "done", "blocked"],
|
|
26
|
-
blocked: ["pending", "in_progress"],
|
|
27
|
-
done: [],
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
function step(machine, allowed, label, current, next) {
|
|
31
|
-
if (!machine.includes(current)) {
|
|
32
|
-
throw new AccError(EXIT.DATA, `unknown ${label} state: ${String(current)}`,
|
|
13
|
+
export function advanceReceipt(current, next) {
|
|
14
|
+
if (!RECEIPT_STATES.includes(current)) {
|
|
15
|
+
throw new AccError(EXIT.DATA, `unknown receipt state: ${String(current)}`,
|
|
33
16
|
{ current, next });
|
|
34
17
|
}
|
|
35
|
-
if (!
|
|
36
|
-
throw new AccError(EXIT.DATA, `unknown
|
|
18
|
+
if (!RECEIPT_STATES.includes(next)) {
|
|
19
|
+
throw new AccError(EXIT.DATA, `unknown receipt state: ${String(next)}`,
|
|
37
20
|
{ current, next });
|
|
38
21
|
}
|
|
39
|
-
// Re-declaring the current state is idempotent. Adapters retry at safe
|
|
40
|
-
// points, and a repeated receipt is not a protocol violation.
|
|
41
22
|
if (current === next) return next;
|
|
42
|
-
if (!
|
|
23
|
+
if (!RECEIPT_NEXT[current].includes(next)) {
|
|
43
24
|
throw new AccError(EXIT.CONFLICT,
|
|
44
|
-
`illegal
|
|
25
|
+
`illegal receipt transition from ${current} to ${next}`, { current, next });
|
|
45
26
|
}
|
|
46
27
|
return next;
|
|
47
28
|
}
|
|
48
|
-
|
|
49
|
-
export function advanceDelivery(current, next) {
|
|
50
|
-
return step(DELIVERY_STATES, DELIVERY_NEXT, "delivery", current, next);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function transitionTask(current, next) {
|
|
54
|
-
return step(TASK_STATES, TASK_NEXT, "task", current, next);
|
|
55
|
-
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
|
|
6
|
+
|
|
7
|
+
import { encode, publishAtomic } from "./atomic-json.mjs";
|
|
8
|
+
import { withRegularNoFollow } from "./safe-file.mjs";
|
|
9
|
+
|
|
10
|
+
const ACTIVE_JOURNAL_VERSION = 2;
|
|
11
|
+
const AUTHORITY_BYTES_LIMIT = 1024;
|
|
12
|
+
const GENERATION_WIDTH = 16;
|
|
13
|
+
const READ_ATTEMPTS = 4;
|
|
14
|
+
|
|
15
|
+
const data = (message, details) => {
|
|
16
|
+
throw new AccError(EXIT.DATA, message, details);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function activeJournalPath(paths, slot) {
|
|
20
|
+
if (slot !== 0 && slot !== 1) data("invalid active journal slot", { slot });
|
|
21
|
+
return path.join(paths.journal, `active.${slot}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function exactKeys(value, expected, filePath) {
|
|
25
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)
|
|
26
|
+
|| Object.keys(value).sort().join("\0") !== [...expected].sort().join("\0")) {
|
|
27
|
+
data("active journal record is not closed", { filePath });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checksum(record) {
|
|
32
|
+
return createHash("sha256").update(JSON.stringify(record)).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function encodeAuthority(record) {
|
|
36
|
+
return encode({ activeJournalChecksum: checksum(record), record });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateActiveRecord(record, filePath) {
|
|
40
|
+
const base = ["activeJournalVersion", "generation", "previousChecksum", "state"];
|
|
41
|
+
if (record?.state === "idle") exactKeys(record, base, filePath);
|
|
42
|
+
else if (record?.state === "open") {
|
|
43
|
+
exactKeys(record, [...base, "transactionId", "firstSequence"], filePath);
|
|
44
|
+
} else data("invalid active journal state", { filePath, state: record?.state });
|
|
45
|
+
|
|
46
|
+
if (record.activeJournalVersion !== ACTIVE_JOURNAL_VERSION) {
|
|
47
|
+
data("unknown active journal version", { filePath,
|
|
48
|
+
activeJournalVersion: record.activeJournalVersion });
|
|
49
|
+
}
|
|
50
|
+
if (typeof record.generation !== "string"
|
|
51
|
+
|| !/^\d{16}$/.test(record.generation)) {
|
|
52
|
+
data("invalid active journal generation", { filePath, generation: record.generation });
|
|
53
|
+
}
|
|
54
|
+
const generation = BigInt(record.generation);
|
|
55
|
+
if (generation === 0n) {
|
|
56
|
+
if (record.state !== "idle" || record.previousChecksum !== null) {
|
|
57
|
+
data("invalid active journal genesis", { filePath });
|
|
58
|
+
}
|
|
59
|
+
} else if (typeof record.previousChecksum !== "string"
|
|
60
|
+
|| !/^[0-9a-f]{64}$/.test(record.previousChecksum)) {
|
|
61
|
+
data("invalid active journal previous checksum", { filePath });
|
|
62
|
+
}
|
|
63
|
+
if (record.state === "open") {
|
|
64
|
+
assertPortableId(record.transactionId, "transaction id");
|
|
65
|
+
if (typeof record.firstSequence !== "string"
|
|
66
|
+
|| !/^\d{16}$/.test(record.firstSequence)) {
|
|
67
|
+
data("invalid active journal first sequence", { filePath,
|
|
68
|
+
firstSequence: record.firstSequence });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return generation;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function decodeAuthority(bytes, filePath, slot) {
|
|
75
|
+
let envelope;
|
|
76
|
+
try {
|
|
77
|
+
envelope = JSON.parse(bytes.toString("utf8"));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
data("invalid active journal authority", { filePath, cause: error.message });
|
|
80
|
+
}
|
|
81
|
+
exactKeys(envelope, ["activeJournalChecksum", "record"], filePath);
|
|
82
|
+
const generation = validateActiveRecord(envelope.record, filePath);
|
|
83
|
+
if (Number(generation % 2n) !== slot) {
|
|
84
|
+
data("active journal generation chain is invalid", { filePath,
|
|
85
|
+
generation: envelope.record.generation, slot });
|
|
86
|
+
}
|
|
87
|
+
if (typeof envelope.activeJournalChecksum !== "string"
|
|
88
|
+
|| envelope.activeJournalChecksum !== checksum(envelope.record)) {
|
|
89
|
+
data("active journal checksum mismatch", { filePath });
|
|
90
|
+
}
|
|
91
|
+
if (!bytes.equals(encodeAuthority(envelope.record))) {
|
|
92
|
+
data("active journal authority is not canonical", { filePath });
|
|
93
|
+
}
|
|
94
|
+
return { checksum: envelope.activeJournalChecksum, generation,
|
|
95
|
+
record: envelope.record, slot };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readSlotBytes(paths, root, slot, openFile) {
|
|
99
|
+
const filePath = activeJournalPath(paths, slot);
|
|
100
|
+
try {
|
|
101
|
+
return await withRegularNoFollow(filePath, root, constants.O_RDONLY,
|
|
102
|
+
async (handle, stat) => {
|
|
103
|
+
if (stat.size < 1 || stat.size > AUTHORITY_BYTES_LIMIT) {
|
|
104
|
+
data("invalid active journal authority size", { filePath, size: stat.size });
|
|
105
|
+
}
|
|
106
|
+
return handle.readFile();
|
|
107
|
+
}, openFile);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code === "ENOENT") return null;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sameBytes(left, right) {
|
|
115
|
+
if (left === null || right === null) return left === right;
|
|
116
|
+
return left.equals(right);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Slot zero is read twice. Because writers alternate slots and generations
|
|
120
|
+
// never repeat, equal reads prove that the pair existed at one instant even if
|
|
121
|
+
// another process published between descriptor opens.
|
|
122
|
+
async function readStableSlots(paths, root, openFile) {
|
|
123
|
+
for (let attempt = 0; attempt < READ_ATTEMPTS; attempt += 1) {
|
|
124
|
+
const slot0 = await readSlotBytes(paths, root, 0, openFile);
|
|
125
|
+
const slot1 = await readSlotBytes(paths, root, 1, openFile);
|
|
126
|
+
const slot0Confirmation = await readSlotBytes(paths, root, 0, openFile);
|
|
127
|
+
if (sameBytes(slot0, slot0Confirmation)) return [slot0, slot1];
|
|
128
|
+
}
|
|
129
|
+
throw new AccError(EXIT.CONFLICT, "active journal changed while being read", {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateTransition(current, previous, filePath) {
|
|
133
|
+
const expected = previous.state === "idle" ? "open" : "idle";
|
|
134
|
+
if (current.state !== expected) {
|
|
135
|
+
data("invalid active journal transition", { filePath,
|
|
136
|
+
previous: previous.state, current: current.state });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function compareGenerations(left, right) {
|
|
141
|
+
if (left.generation < right.generation) return -1;
|
|
142
|
+
if (left.generation > right.generation) return 1;
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function selectAuthority(paths, bytes) {
|
|
147
|
+
// Decode every present slot before selection. Falling back from a corrupt
|
|
148
|
+
// latest slot to an older valid peer would turn corruption into rollback.
|
|
149
|
+
const found = bytes.map((value, slot) => value === null
|
|
150
|
+
? null
|
|
151
|
+
: decodeAuthority(value, activeJournalPath(paths, slot), slot)).filter(Boolean);
|
|
152
|
+
if (found.length === 0) return null;
|
|
153
|
+
if (found.length === 1) {
|
|
154
|
+
const [only] = found;
|
|
155
|
+
if (only.slot === 0 && only.generation === 0n) return only;
|
|
156
|
+
data("active journal authority peer is missing", { slot: only.slot,
|
|
157
|
+
generation: only.record.generation });
|
|
158
|
+
}
|
|
159
|
+
found.sort(compareGenerations);
|
|
160
|
+
const [previous, current] = found;
|
|
161
|
+
if (current.generation !== previous.generation + 1n) {
|
|
162
|
+
data("active journal generation chain is invalid", {
|
|
163
|
+
previous: previous.record.generation, current: current.record.generation });
|
|
164
|
+
}
|
|
165
|
+
if (current.record.previousChecksum !== previous.checksum) {
|
|
166
|
+
data("active journal checksum chain is invalid", { slot: current.slot });
|
|
167
|
+
}
|
|
168
|
+
validateTransition(current.record, previous.record,
|
|
169
|
+
activeJournalPath(paths, current.slot));
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function readCurrent(paths, root, openFile) {
|
|
174
|
+
return selectAuthority(paths, await readStableSlots(paths, root, openFile));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const genesis = () => ({ activeJournalVersion: ACTIVE_JOURNAL_VERSION,
|
|
178
|
+
generation: "0".repeat(GENERATION_WIDTH), previousChecksum: null, state: "idle" });
|
|
179
|
+
|
|
180
|
+
export async function initialiseActiveJournal(paths, options) {
|
|
181
|
+
const current = await readCurrent(paths, options.root);
|
|
182
|
+
if (current !== null) return current.record;
|
|
183
|
+
try {
|
|
184
|
+
await publishAtomic(activeJournalPath(paths, 0), encodeAuthority(genesis()), options);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.code !== EXIT.CONFLICT) throw error;
|
|
187
|
+
}
|
|
188
|
+
return readActiveJournal(paths, options.root);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function readActiveJournal(paths, root, openFile) {
|
|
192
|
+
const current = await readCurrent(paths, root, openFile);
|
|
193
|
+
if (current === null) data("active journal has no authority", { journal: paths.journal });
|
|
194
|
+
return current.record;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function nextGeneration(current) {
|
|
198
|
+
const next = current.generation + 1n;
|
|
199
|
+
const generation = next.toString().padStart(GENERATION_WIDTH, "0");
|
|
200
|
+
if (generation.length !== GENERATION_WIDTH) {
|
|
201
|
+
data("active journal generation is exhausted", { current: current.record.generation });
|
|
202
|
+
}
|
|
203
|
+
return generation;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function appendTransition(paths, options, fields, expected) {
|
|
207
|
+
const current = await readCurrent(paths, options.root, options.openFile);
|
|
208
|
+
if (current === null || !expected(current.record)) {
|
|
209
|
+
throw new AccError(EXIT.CONFLICT, "active journal transition conflicts",
|
|
210
|
+
{ current: current?.record ?? null, next: fields });
|
|
211
|
+
}
|
|
212
|
+
const record = { activeJournalVersion: ACTIVE_JOURNAL_VERSION,
|
|
213
|
+
generation: nextGeneration(current), previousChecksum: current.checksum, ...fields };
|
|
214
|
+
validateActiveRecord(record, activeJournalPath(paths, 1 - current.slot));
|
|
215
|
+
await publishAtomic(activeJournalPath(paths, 1 - current.slot), encodeAuthority(record),
|
|
216
|
+
{ ...options, tmpDir: options.tmpDir ?? paths.tmp ?? path.join(options.root, "tmp"),
|
|
217
|
+
replace: true });
|
|
218
|
+
return record;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function activateJournal(paths, options, entry) {
|
|
222
|
+
return appendTransition(paths, options, { state: "open",
|
|
223
|
+
transactionId: entry.transactionId, firstSequence: entry.firstSequence },
|
|
224
|
+
current => current.state === "idle");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function idleJournal(paths, options, transactionId) {
|
|
228
|
+
return appendTransition(paths, options, { state: "idle" },
|
|
229
|
+
current => current.state === "open" && current.transactionId === transactionId);
|
|
230
|
+
}
|