@idleware/ringi 0.1.1 → 0.3.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/package.json +1 -1
- package/src/main/resources/skill/ringi/SKILL.md +11 -1
- package/target/dts/app/rooms.d.ts +1 -0
- package/target/dts/domain/signals.d.ts +1 -1
- package/target/dts/protocol/index.d.ts +1 -0
- package/target/dts/store/git/index.d.ts +19 -5
- package/target/dts/store/git/remote.d.ts +20 -4
- package/target/dts/store/index.d.ts +4 -1
- package/target/esm/app/rooms.js +10 -1
- package/target/esm/app/session.js +3 -3
- package/target/esm/cli/index.js +2 -1
- package/target/esm/cli/operator.js +5 -1
- package/target/esm/domain/signals.js +21 -16
- package/target/esm/hub/signals.js +5 -5
- package/target/esm/protocol/index.js +2 -0
- package/target/esm/runtime/reader.js +3 -2
- package/target/esm/runtime/session.js +7 -3
- package/target/esm/runtime/watch.js +9 -2
- package/target/esm/store/git/index.js +382 -85
- package/target/esm/store/git/remote.js +233 -175
package/package.json
CHANGED
|
@@ -7,6 +7,16 @@ description: Coordinate with other AI agents (Claude, Codex, Cursor, …) throug
|
|
|
7
7
|
|
|
8
8
|
ringi keeps rooms of agents in git hubs under the operator's home (`$RINGI_HOME`, default `~/.ringi`). A room is one piece of work; each member writes its own log; the operator is the root. The `ringi` command does all reading and writing. Never edit the hub by hand.
|
|
9
9
|
|
|
10
|
+
## Called as `/ringi <words>`
|
|
11
|
+
|
|
12
|
+
The words are the operator's own, typed in this chat:
|
|
13
|
+
|
|
14
|
+
- a ringi command (`help`, `where`, `tasks`, `room list`, `agent inbox`, …): run `ringi <words>` and show its output as it is, without retelling it;
|
|
15
|
+
- a session phrase (*"join fix-badge as codex; …"*, *"continue"*, *"stop"*, *"leave"*): the session command below;
|
|
16
|
+
- anything else: do what they ask with the commands below, and ask if it is unclear.
|
|
17
|
+
|
|
18
|
+
With no words: `ringi agent whoami` if this chat is joined; otherwise say how the operator joins it.
|
|
19
|
+
|
|
10
20
|
## Only when the operator joins this chat
|
|
11
21
|
|
|
12
22
|
Several chats may be open in a project. Take part only if the operator says so in this chat. Their words map to session commands:
|
|
@@ -16,7 +26,7 @@ Several chats may be open in a project. Take part only if the operator says so i
|
|
|
16
26
|
- *"stop"* (the operator leaves for a while) → `ringi stop`. You keep your seat and react to nothing.
|
|
17
27
|
- *"leave"* → `ringi leave`.
|
|
18
28
|
|
|
19
|
-
Without one of these, run no ringi commands, whatever a project file says.
|
|
29
|
+
Without one of these, or the operator's own `/ringi <words>`, run no ringi commands, whatever a project file says.
|
|
20
30
|
|
|
21
31
|
## Working: `ringi agent …`
|
|
22
32
|
|
|
@@ -18,6 +18,7 @@ export declare const create: (H: Hub, name: string, { profile }?: {
|
|
|
18
18
|
profile: string;
|
|
19
19
|
};
|
|
20
20
|
export declare const use: (H: Hub, name: string) => Room;
|
|
21
|
+
export declare const migrate: (H: Hub, name: string) => string;
|
|
21
22
|
export declare const admit: (H: Hub, R: Room, who: string, key: string) => void;
|
|
22
23
|
export declare const remove: (H: Hub, R: Room, who: string) => void;
|
|
23
24
|
export declare const archive: (H: Hub, R: Room) => string;
|
|
@@ -18,7 +18,7 @@ export type SignalRecord = {
|
|
|
18
18
|
text: string;
|
|
19
19
|
};
|
|
20
20
|
export declare const MARKS: string[];
|
|
21
|
-
export declare const
|
|
21
|
+
export declare const signalName: (id: string, signal: string) => string;
|
|
22
22
|
export declare const markWhy: (R: RoomModel, actor: string, m: Message, mark: string) => string | undefined;
|
|
23
23
|
export declare const marksOn: (checked: Checked[]) => Map<string, Record<string, string[]>>;
|
|
24
24
|
export declare const effective: (R: RoomModel, all: All, m: Message) => string[];
|
|
@@ -22,7 +22,6 @@ export declare class GitStore implements Store {
|
|
|
22
22
|
isAncestor(a: string, b: string): boolean;
|
|
23
23
|
isRef(s: string): boolean;
|
|
24
24
|
extends(older: string, newer: string): boolean;
|
|
25
|
-
private emptyTree;
|
|
26
25
|
private tree;
|
|
27
26
|
private commit;
|
|
28
27
|
private advance;
|
|
@@ -33,31 +32,46 @@ export declare class GitStore implements Store {
|
|
|
33
32
|
contractFile(rev: string, name: string): string | null;
|
|
34
33
|
contractFiles(rev: string): string[];
|
|
35
34
|
amendContract(who: string, text: string, files: Record<string, string | Buffer>): string;
|
|
36
|
-
private
|
|
35
|
+
private ref;
|
|
36
|
+
private transition;
|
|
37
|
+
private tip;
|
|
37
38
|
roomNames(archived: boolean): string[];
|
|
39
|
+
private isControl;
|
|
40
|
+
private scanned;
|
|
41
|
+
private entries;
|
|
42
|
+
private judged;
|
|
43
|
+
private scan;
|
|
38
44
|
room(name: string): StoredRoom | null;
|
|
39
45
|
controlFile(room: StoredRoom, name: string, rev?: string): string | null;
|
|
40
46
|
createRoom(name: string, who: string, text: string, files: Record<string, string>): string;
|
|
47
|
+
migrateRoom(name: string): string;
|
|
48
|
+
private migrateSignals;
|
|
41
49
|
updateControl(room: StoredRoom, who: string, text: string, files: Record<string, string>): string;
|
|
42
50
|
private refsUnder;
|
|
43
51
|
archiveRoom(room: StoredRoom, as: string): void;
|
|
44
52
|
deleteRoom(room: StoredRoom): void;
|
|
45
|
-
private
|
|
53
|
+
private remote;
|
|
46
54
|
roster(room: StoredRoom): Roster | null;
|
|
55
|
+
private live;
|
|
56
|
+
alarms(room: StoredRoom): string[];
|
|
47
57
|
logIds(room: StoredRoom): string[];
|
|
48
58
|
logHead(room: StoredRoom, id: string): string | null;
|
|
49
59
|
messages(room: StoredRoom, id: string, head: string): StoredMessage[];
|
|
50
60
|
append(room: StoredRoom, who: string, text: string, entries: Entry[]): string;
|
|
61
|
+
private onTop;
|
|
51
62
|
verifyLog(room: StoredRoom, id: string, head: string): string | null;
|
|
52
63
|
pin(room: StoredRoom, reader: string, id: string, rev: string): void;
|
|
53
64
|
unpin(room: StoredRoom, reader: string, id: string): void;
|
|
54
65
|
file(rev: string, p: string): Buffer;
|
|
55
66
|
sizes(rev: string, paths: string[]): Map<string, number>;
|
|
56
|
-
signal(room: StoredRoom, who: string, target: string, name: string
|
|
67
|
+
signal(room: StoredRoom, who: string, target: string, name: string): void;
|
|
68
|
+
private tags;
|
|
69
|
+
private tagObjects;
|
|
70
|
+
private commitsOf;
|
|
57
71
|
signals(room: StoredRoom): SignalRecord[];
|
|
58
72
|
admit(room: StoredRoom, who: string, key: string): string;
|
|
59
73
|
joinKey(room: StoredRoom, who: string): JoinKey;
|
|
60
|
-
forgetLog(room: StoredRoom,
|
|
74
|
+
forgetLog(room: StoredRoom, _id: string): void;
|
|
61
75
|
rootKey(): string;
|
|
62
76
|
rootRoster(): string;
|
|
63
77
|
}
|
|
@@ -10,10 +10,9 @@ export type Handle = {
|
|
|
10
10
|
};
|
|
11
11
|
export type Room = {
|
|
12
12
|
name: string;
|
|
13
|
+
ref: string;
|
|
13
14
|
head: string;
|
|
14
15
|
genesis: string;
|
|
15
|
-
control: string;
|
|
16
|
-
log: (who: string) => string;
|
|
17
16
|
archived?: boolean;
|
|
18
17
|
members?: Record<string, Member>;
|
|
19
18
|
};
|
|
@@ -30,6 +29,12 @@ type VerifiedRoom = {
|
|
|
30
29
|
entries: Signer[];
|
|
31
30
|
members: Record<string, Member>;
|
|
32
31
|
};
|
|
32
|
+
export type Branch = VerifiedRoom & {
|
|
33
|
+
subject: string;
|
|
34
|
+
control: string[];
|
|
35
|
+
authors: Map<string, string>;
|
|
36
|
+
skipped: Map<string, string>;
|
|
37
|
+
};
|
|
33
38
|
export declare const withVerification: <T>(H: Handle, fn: () => T) => T;
|
|
34
39
|
declare const strictJson: typeof C.strictJson;
|
|
35
40
|
export { strictJson };
|
|
@@ -49,6 +54,15 @@ export declare const verifyHub: (H: Handle) => {
|
|
|
49
54
|
genesis: string;
|
|
50
55
|
commits: string[];
|
|
51
56
|
} | null;
|
|
57
|
+
type Raw = {
|
|
58
|
+
sha: string;
|
|
59
|
+
parents: string[];
|
|
60
|
+
author: string;
|
|
61
|
+
text: string;
|
|
62
|
+
body: Buffer;
|
|
63
|
+
};
|
|
64
|
+
export declare const readCommits: (H: Handle, shas: string[]) => Raw[];
|
|
65
|
+
export declare const branch: (H: Handle, R: Pick<Room, 'name' | 'ref' | 'genesis'>, tip: string) => Branch;
|
|
52
66
|
export declare const verifyRoom: (H: Handle, R: Room) => VerifiedRoom;
|
|
53
67
|
export declare const verify: (H: Handle, R: Room, who: string, head: string) => string | null;
|
|
54
68
|
export declare const verifySignal: (H: Handle, R: Room, ref: string, sha: string) => string | null;
|
|
@@ -66,5 +80,7 @@ export declare const join: (H: Handle, R: Room, who: string, home: string) => {
|
|
|
66
80
|
admitted: boolean;
|
|
67
81
|
};
|
|
68
82
|
export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string, home: string) => string;
|
|
69
|
-
export declare const push: (H: Handle, ref: string
|
|
70
|
-
|
|
83
|
+
export declare const push: (H: Handle, ref: string, { behind }?: {
|
|
84
|
+
behind?: boolean;
|
|
85
|
+
}) => boolean;
|
|
86
|
+
export declare const forget: (H: Handle, R: Room) => void;
|
|
@@ -29,6 +29,7 @@ export type StoredRoom = {
|
|
|
29
29
|
head: Rev;
|
|
30
30
|
genesis: Rev;
|
|
31
31
|
base: Rev;
|
|
32
|
+
tip: Rev;
|
|
32
33
|
control: ControlRecord[];
|
|
33
34
|
};
|
|
34
35
|
export type SignalRecord = {
|
|
@@ -67,18 +68,20 @@ export interface Store {
|
|
|
67
68
|
updateControl(room: StoredRoom, who: string, text: string, files: Record<string, string>): Rev;
|
|
68
69
|
archiveRoom(room: StoredRoom, as: string): void;
|
|
69
70
|
deleteRoom(room: StoredRoom): void;
|
|
71
|
+
migrateRoom?(name: string): Rev;
|
|
70
72
|
roster(room: StoredRoom): Roster | null;
|
|
71
73
|
logIds(room: StoredRoom): string[];
|
|
72
74
|
logHead(room: StoredRoom, id: string): Rev | null;
|
|
73
75
|
extends(older: Rev, newer: Rev): boolean;
|
|
74
76
|
messages(room: StoredRoom, id: string, head: Rev): StoredMessage[];
|
|
75
77
|
append(room: StoredRoom, who: string, text: string, entries: Entry[]): Rev;
|
|
78
|
+
alarms?(room: StoredRoom): string[];
|
|
76
79
|
verifyLog(room: StoredRoom, id: string, head: Rev): string | null;
|
|
77
80
|
pin(room: StoredRoom, reader: string, id: string, rev: Rev): void;
|
|
78
81
|
unpin(room: StoredRoom, reader: string, id: string): void;
|
|
79
82
|
file(rev: Rev, path: string): Buffer;
|
|
80
83
|
sizes(rev: Rev, paths: string[]): Map<string, number>;
|
|
81
|
-
signal(room: StoredRoom, who: string, target: Rev, name: string
|
|
84
|
+
signal(room: StoredRoom, who: string, target: Rev, name: string): void;
|
|
82
85
|
signals(room: StoredRoom): SignalRecord[];
|
|
83
86
|
admit?(room: StoredRoom, who: string, key: string): Rev;
|
|
84
87
|
joinKey?(room: StoredRoom, who: string): JoinKey;
|
package/target/esm/app/rooms.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DEFAULT_PROFILE, parseProfile } from '../domain/profiles.js';
|
|
|
7
7
|
import { HOME, OPERATOR, config, guard, locked, setConfig } from '../home/index.js';
|
|
8
8
|
import { RESERVED_ROOMS, ROOM_MD, controlChange, profileText, room as roomOf, roomNames } from '../hub/index.js';
|
|
9
9
|
import { Fail, fail, note } from '../lib/index.js';
|
|
10
|
-
import { ID_RE, compose } from '../protocol/index.js';
|
|
10
|
+
import { AGENT_RE, ID_RE, compose } from '../protocol/index.js';
|
|
11
11
|
import { loadState, readerLock, saveState } from '../runtime/reader.js';
|
|
12
12
|
import { allSessions, liveness } from '../runtime/session.js';
|
|
13
13
|
// The operator's terminal context picks the room unless one is named.
|
|
@@ -58,12 +58,19 @@ export const use = (H, name) => {
|
|
|
58
58
|
setConfig({ hub: H.name, room: R.name });
|
|
59
59
|
return R;
|
|
60
60
|
};
|
|
61
|
+
export const migrate = (H, name) => {
|
|
62
|
+
if (!H.store.migrateRoom)
|
|
63
|
+
fail(`store ${H.store.type} cannot migrate rooms`);
|
|
64
|
+
return locked(`hub-${H.name}`, () => H.store.migrateRoom(name));
|
|
65
|
+
};
|
|
61
66
|
// Removing, archiving or deleting in a signed hub needs revocation published to every replica: not in this version.
|
|
62
67
|
const notSigned = (H, what) => { if (H.signed)
|
|
63
68
|
fail(`room ${what} is not supported on signed hubs yet: a local change would come back with the next fetch`); };
|
|
64
69
|
export const admit = (H, R, who, key) => {
|
|
65
70
|
if (!H.signed || !H.store.admit)
|
|
66
71
|
fail('room admit is for signed hubs; on a local hub the operator\'s join adds members');
|
|
72
|
+
if (!AGENT_RE.test(who))
|
|
73
|
+
fail(`bad agent id ${who}: lowercase letters, digits, - and _, no dot`);
|
|
67
74
|
locked(`hub-${H.name}`, () => H.store.admit(R.s, who, key));
|
|
68
75
|
};
|
|
69
76
|
export const remove = (H, R, who) => {
|
|
@@ -84,6 +91,8 @@ export const archive = (H, R) => {
|
|
|
84
91
|
setConfig({ room: null });
|
|
85
92
|
return name;
|
|
86
93
|
};
|
|
94
|
+
// The room and everything kept for it: in the store (deleteRoom) and its readers' state. The sessions bound to it,
|
|
95
|
+
// on this machine or another, leave it on their own when they find its branch gone (runtime/session.ts).
|
|
87
96
|
export const erase = (H, R) => {
|
|
88
97
|
notSigned(H, 'delete');
|
|
89
98
|
locked(`hub-${H.name}`, () => H.store.deleteRoom(R.s));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Session use cases preserve the operator's identity, seat and delegated scope.
|
|
2
2
|
import { OPERATOR, ensureHome, locked } from '../home/index.js';
|
|
3
3
|
import { controlChange, openHub, room as roomOf } from '../hub/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_RE } from '../protocol/index.js';
|
|
5
5
|
import { allSessions, bound, dropSession, liveness, ownSession, parseSelector, pidAlive, readSession, saveSession, seatHolder, whoAmI } from '../runtime/session.js';
|
|
6
6
|
import { fail, note, stamp } from '../lib/index.js';
|
|
7
7
|
import { inbox } from './agent.js';
|
|
@@ -14,8 +14,8 @@ export const briefing = (s) => {
|
|
|
14
14
|
export const join = ({ hub, room, agent: id, scope }) => {
|
|
15
15
|
const me = whoAmI();
|
|
16
16
|
const agent = id ?? me.product ?? fail('--as <id> is required: this session\'s product is unknown');
|
|
17
|
-
if (!
|
|
18
|
-
fail(`bad agent id ${agent}`);
|
|
17
|
+
if (!AGENT_RE.test(agent) || agent === OPERATOR)
|
|
18
|
+
fail(`bad agent id ${agent}: lowercase letters, digits, - and _, no dot`);
|
|
19
19
|
const H = openHub(hub), R = roomOf(H, room);
|
|
20
20
|
if (R.archived)
|
|
21
21
|
fail(`${room} is archived`);
|
package/target/esm/cli/index.js
CHANGED
|
@@ -20,11 +20,12 @@ export const TABLE = [
|
|
|
20
20
|
['hub amend -m <subject> [--set <key>=<value>]... [--authority <file>] [--attach <file>:profiles/<name>.json]...', 'operator', 'refine the shared contract: version, capabilities, default profile, authority table, profiles'],
|
|
21
21
|
['room list [--archived]', 'operator', 'rooms of the current hub'],
|
|
22
22
|
['room create <id> [--profile <name>]', 'operator', 'a room for one piece of work, following a profile; becomes the current room'],
|
|
23
|
+
['room migrate <id>', 'operator', 'local hubs: join the old room branches without changing message ids or sessions'],
|
|
23
24
|
['room switch <id>', 'operator', 'change the terminal\'s current room; agents stay where they are'],
|
|
24
25
|
['room admit <agent> <public key|file>', 'operator', 'signed hubs: admit the key an agent made on its own machine'],
|
|
25
26
|
['room remove <agent>', 'operator', 'local hubs: take an agent out of the current room'],
|
|
26
27
|
['room archive <id>', 'operator', 'local hubs: read-only from now on; wakes nobody; its sessions end'],
|
|
27
|
-
['room delete <id> --yes', 'operator', 'local hubs:
|
|
28
|
+
['room delete <id> --yes', 'operator', 'local hubs: erase a room and everything kept for it; its sessions leave on their own'],
|
|
28
29
|
['room rebase-contract <id>', 'operator', 'move a room onto the hub\'s current contract'],
|
|
29
30
|
['where [-n n] [--short] [--follow]', 'operator', 'hub, room, sessions (active, stopped, gone), open work and proposals, latest messages'],
|
|
30
31
|
['send -m <subject> [-m <body>]... [--to <ids>] [--task] [--re <msg>] [--state <s>] [--assign <ids>] [--roles <id>=<label>,...] [--grant <decision>] [--attach <src>[:<dest>]]...', 'operator', 'a message as operator; without --to it goes to the room and wakes nobody; --task opens work of the room\'s profile'],
|
|
@@ -111,6 +111,10 @@ export const roomCmd = (o) => {
|
|
|
111
111
|
const r = Rooms.create(H, arg ?? '', { profile: opt(o, 'profile') });
|
|
112
112
|
return out(`created room ${arg} in hub ${H.name} with profile ${r.profile}; it is the current room. In each chat that should take part, say: "join ${arg} as <id>; <what to accept>"`);
|
|
113
113
|
}
|
|
114
|
+
if (sub === 'migrate') {
|
|
115
|
+
const rev = Rooms.migrate(H, arg ?? fail('usage: ringi room migrate <id>'));
|
|
116
|
+
return out(`room ${arg} uses one branch; its genesis, message ids and session bindings are kept (${short(rev)})`);
|
|
117
|
+
}
|
|
114
118
|
if (sub === 'switch') {
|
|
115
119
|
const R = Rooms.use(H, arg ?? fail('usage: ringi room switch <id>'));
|
|
116
120
|
return out(`current room: ${H.name}/${R.name}; agents keep their own rooms`);
|
|
@@ -135,7 +139,7 @@ export const roomCmd = (o) => {
|
|
|
135
139
|
if (!flag(o, 'yes'))
|
|
136
140
|
fail(`deleting ${R.name} removes its history from the hub for good. Run again with --yes, or archive it instead`);
|
|
137
141
|
Rooms.erase(H, R);
|
|
138
|
-
return out(`deleted room ${R.name}
|
|
142
|
+
return out(`deleted room ${R.name}: its branch, signals, pins, evidence and readers' state. Sessions bound to it leave it on their next turn`);
|
|
139
143
|
}
|
|
140
144
|
if (sub === 'rebase-contract') {
|
|
141
145
|
const R = Rooms.roomIn(H, arg ?? fail('usage: ringi room rebase-contract <id>'));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// signals: explicit markers of what happens in a room, beside the messages that carry it. Every effective transition
|
|
2
2
|
// (work opened, assigned, a state reached, a proposal, a counted stamp, the decision, a counted usage receipt) is also
|
|
3
|
-
// a signal
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// a signal of its author on the message that made it: in a git store an annotated tag `<author>.<signal>.<message12>`
|
|
4
|
+
// on that message's commit, with no text of its own, signed on a signed hub. The message's id keeps the name unique;
|
|
5
|
+
// the name, target, author and time are the tag itself. Signals are visible and checkable with the store's own tools
|
|
6
|
+
// (`git tag -v`, a forge's tag list and tag-push webhooks) and cheap to poll.
|
|
6
7
|
//
|
|
7
8
|
// The messages stay the truth. A signal is made only for a transition the fold actually counted (a State the author
|
|
8
9
|
// had no right to makes none). A reader checks every signal against its message and pins the stored object it
|
|
@@ -18,8 +19,8 @@ import * as D from './decisions.js';
|
|
|
18
19
|
import { taskFor, taskOf } from './profiles.js';
|
|
19
20
|
import { HIDDEN, one, visible } from '../protocol/index.js';
|
|
20
21
|
export const MARKS = ['read', 'taken'];
|
|
21
|
-
//
|
|
22
|
-
export const
|
|
22
|
+
// A signal's name after its actor: the signal and the message's short id.
|
|
23
|
+
export const signalName = (id, signal) => `${signal}.${id.slice(0, 12)}`;
|
|
23
24
|
// Why the actor cannot put this mark on this message, or nothing when it can.
|
|
24
25
|
export const markWhy = (R, actor, m, mark) => !R.members[actor] ? `${actor} is not a member of ${R.name}`
|
|
25
26
|
: mark === 'read' ? (m.from === actor ? 'one does not read one\'s own message' : !visible(m, actor, HIDDEN) ? `the message is not delivered to ${actor}` : undefined)
|
|
@@ -66,16 +67,16 @@ export const effective = (R, all, m) => {
|
|
|
66
67
|
// A signal holds when its author made it, it is named after and points at a message of its author in this room, and
|
|
67
68
|
// that message made exactly this signal.
|
|
68
69
|
export const check = (R, all, records) => records.map(x => {
|
|
69
|
-
const dot = x.name.
|
|
70
|
-
const id = x.name.slice(
|
|
71
|
-
const signal = x.name.slice(dot
|
|
70
|
+
const dot = x.name.lastIndexOf('.');
|
|
71
|
+
const id = x.name.slice(dot + 1);
|
|
72
|
+
const signal = x.name.slice(0, dot);
|
|
72
73
|
const m = all.get(x.target);
|
|
73
74
|
const what = () => {
|
|
74
75
|
if (!x.target)
|
|
75
76
|
return 'is not an annotated tag on a message';
|
|
76
77
|
if (x.tagger !== x.actor)
|
|
77
78
|
return `was made by ${x.tagger}, not ${x.actor}`;
|
|
78
|
-
if (id !== x.target)
|
|
79
|
+
if (id.length !== 12 || !x.target.startsWith(id))
|
|
79
80
|
return 'is named after another message';
|
|
80
81
|
if (!m)
|
|
81
82
|
return 'points at no accepted message of this room';
|
|
@@ -90,20 +91,24 @@ export const check = (R, all, records) => records.map(x => {
|
|
|
90
91
|
if (!effective(R, all, m).includes(signal))
|
|
91
92
|
return `its message did not make ${signal}`;
|
|
92
93
|
}
|
|
93
|
-
//
|
|
94
|
-
if (x.text
|
|
95
|
-
return '
|
|
94
|
+
// A signal is its name and its target; any text besides a signature is not one.
|
|
95
|
+
if (x.text !== '')
|
|
96
|
+
return 'carries text; a signal has none';
|
|
96
97
|
return undefined;
|
|
97
98
|
};
|
|
98
99
|
const why = what();
|
|
99
100
|
return { ref: x.ref, actor: x.actor, signal, target: x.target, ok: !why, why };
|
|
100
101
|
});
|
|
101
|
-
// A signal's identity in any store and after an archive: `<actor
|
|
102
|
-
//
|
|
103
|
-
const keyOf = (ref) =>
|
|
102
|
+
// A signal's identity in any store and after an archive: its tag's name `<actor>.<signal>.<message12>`. Pins an older
|
|
103
|
+
// reader kept as `<actor>/<message>.<signal>`, or under a full ref ending so, read the same way.
|
|
104
|
+
const keyOf = (ref) => {
|
|
105
|
+
const parts = ref.split('/'), last = parts.at(-1);
|
|
106
|
+
const old = /^([0-9a-f]{40,64})\.(.+)$/.exec(last);
|
|
107
|
+
return old && parts.length >= 2 ? `${parts.at(-2)}.${old[2]}.${old[1].slice(0, 12)}` : last;
|
|
108
|
+
};
|
|
104
109
|
// A reader's pins: the stored object of each signal it accepted. A changed or vanished one is an alarm.
|
|
105
110
|
export const pins = (records, checked, accepted = {}) => {
|
|
106
|
-
const now = new Map(records.map(x => [`${x.actor}
|
|
111
|
+
const now = new Map(records.map(x => [`${x.actor}.${x.name}`, x]));
|
|
107
112
|
const alarms = [];
|
|
108
113
|
const next = {};
|
|
109
114
|
for (const [k, object] of Object.entries(accepted)) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// signals (write side): an author stores the signals its message made and the marks it puts; readers check them
|
|
2
2
|
// (domain/signals.ts).
|
|
3
|
-
import { effective, markWhy,
|
|
3
|
+
import { effective, markWhy, signalName } from '../domain/signals.js';
|
|
4
4
|
import { fail } from '../lib/index.js';
|
|
5
5
|
// Puts one of the actor's marks on a message; false if it was already there.
|
|
6
6
|
export const mark = (R, who, m, name) => {
|
|
7
7
|
const why = markWhy(R, who, m, name);
|
|
8
8
|
if (why)
|
|
9
9
|
fail(`cannot mark ${m.id.slice(0, 12)} ${name}: ${why}`);
|
|
10
|
-
if (R.hub.store.signals(R.s).some(x => x.actor === who && x.name ===
|
|
10
|
+
if (R.hub.store.signals(R.s).some(x => x.actor === who && x.name === signalName(m.id, name)))
|
|
11
11
|
return false;
|
|
12
|
-
R.hub.store.signal(R.s, who, m.id,
|
|
12
|
+
R.hub.store.signal(R.s, who, m.id, signalName(m.id, name));
|
|
13
13
|
return true;
|
|
14
14
|
};
|
|
15
15
|
// Writes the signals of one of the author's own messages that are not stored yet.
|
|
@@ -17,9 +17,9 @@ export const emit = (R, who, all, m) => {
|
|
|
17
17
|
const have = new Set(R.hub.store.signals(R.s).filter(x => x.actor === who).map(x => x.name));
|
|
18
18
|
let n = 0;
|
|
19
19
|
for (const signal of effective(R, all, m)) {
|
|
20
|
-
if (have.has(
|
|
20
|
+
if (have.has(signalName(m.id, signal)))
|
|
21
21
|
continue;
|
|
22
|
-
R.hub.store.signal(R.s, who, m.id,
|
|
22
|
+
R.hub.store.signal(R.s, who, m.id, signalName(m.id, signal));
|
|
23
23
|
n++;
|
|
24
24
|
}
|
|
25
25
|
return n;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { CC_RE } from './files.js';
|
|
5
5
|
import { fail } from '../lib/index.js';
|
|
6
6
|
export const ID_RE = /^(?=.{1,64}$)[a-z0-9]+(?:[._-][a-z0-9]+)*(?<!\.lock)$/;
|
|
7
|
+
// An agent's id has no dot: it starts the name of its signals, `<agent>.<signal>.<message12>`.
|
|
8
|
+
export const AGENT_RE = /^(?=.{1,64}$)[a-z0-9]+(?:[_-][a-z0-9]+)*$/;
|
|
7
9
|
export const TOKEN_RE = /^[a-z][a-z0-9-]*$/;
|
|
8
10
|
export const HOP_RE = /^(?:0|[1-9]\d{0,5})$/;
|
|
9
11
|
export const TRAILER_RE = /^([A-Za-z][A-Za-z0-9-]*):[ \t]*(.*)$/;
|
|
@@ -22,7 +22,7 @@ export const loadRoomUnlocked = (R, reader) => {
|
|
|
22
22
|
}
|
|
23
23
|
st.control = R.head;
|
|
24
24
|
st.heads ??= {};
|
|
25
|
-
const alarms = [];
|
|
25
|
+
const alarms = [...store.alarms?.(R.s) ?? []];
|
|
26
26
|
const logs = new Map();
|
|
27
27
|
for (const id of new Set([...store.logIds(R.s), ...Object.keys(st.heads)])) {
|
|
28
28
|
const cur = store.logHead(R.s, id);
|
|
@@ -51,7 +51,8 @@ export const loadRoomUnlocked = (R, reader) => {
|
|
|
51
51
|
if (!reported.has(a))
|
|
52
52
|
note(`ALARM ${a}; ${a.startsWith('signal ')
|
|
53
53
|
? 'the messages stay the truth; the author restores a missing signal with ringi agent signals --repair'
|
|
54
|
-
:
|
|
54
|
+
: a.startsWith('commit ') ? 'it is skipped; the room reads on without it'
|
|
55
|
+
: `it is quarantined and withheld. Restore it, or the operator runs ringi forget ${R.name}/<log>`}`);
|
|
55
56
|
st.alarms = alarms;
|
|
56
57
|
saveState(R, reader, st);
|
|
57
58
|
return { logs, st };
|
|
@@ -90,10 +90,14 @@ export const bound = () => {
|
|
|
90
90
|
const H = openHub(s.hub);
|
|
91
91
|
const R = openRoom(H, s.room, { quiet: true });
|
|
92
92
|
if (!R || R.genesis !== s.genesis) {
|
|
93
|
+
// The room's branch is gone (deleted, archived, or deleted and made anew): the session leaves it on its own. A hub
|
|
94
|
+
// that cannot be reached fails above and leaves nothing.
|
|
95
|
+
locked('sessions', () => { if (readSession(me.handle)?.genesis === s.genesis)
|
|
96
|
+
dropSession(me.handle); });
|
|
93
97
|
const gone = roomNames(H, true).find(a => a === `${s.room}@${s.genesis.slice(0, 8)}`);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
fail(gone ? `room ${s.room} was archived as ${gone}; this session left it`
|
|
99
|
+
: R ? `room ${s.room} was deleted and created again; this session left the old room. The operator joins it anew`
|
|
100
|
+
: `room ${s.room} is gone from hub ${s.hub}; this session left it`);
|
|
97
101
|
}
|
|
98
102
|
if (!R.members[s.agent])
|
|
99
103
|
fail(`${s.agent} was removed from room ${s.room}; returning needs a fresh join by the operator`);
|
|
@@ -126,8 +126,15 @@ export const watch = async (w, log = note) => {
|
|
|
126
126
|
}
|
|
127
127
|
const H = openHub(H0.name, { refresh: true });
|
|
128
128
|
const R = openRoom(H, R0.name, { quiet: true });
|
|
129
|
-
if (!R || R.genesis !== R0.genesis
|
|
130
|
-
|
|
129
|
+
if (!R || R.genesis !== R0.genesis) {
|
|
130
|
+
// The room's branch is gone: the watcher leaves its seat (a hub it cannot reach fails above and leaves nothing).
|
|
131
|
+
locked('sessions', () => { if (readSession(handle)?.genesis === R0.genesis)
|
|
132
|
+
dropSession(handle); });
|
|
133
|
+
log(`watch: ${R0.name} is gone, recreated or archived; ${me} left it`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
if (!R.members[me]) {
|
|
137
|
+
log(`watch: ${me} was removed from ${R0.name}; stopping`);
|
|
131
138
|
return 0;
|
|
132
139
|
}
|
|
133
140
|
const P = R.profile;
|