@ccmsg/cli 0.8.2 → 0.9.1
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 +2 -2
- package/src/auth/auth.ts +14 -14
- package/src/auth/http.ts +6 -6
- package/src/auth/records.ts +1 -1
- package/src/auth/topic.ts +1 -1
- package/src/cli.ts +21 -24
- package/src/daemon/control.ts +2 -2
- package/src/daemon/registry.ts +7 -7
- package/src/daemon/snapshot.ts +12 -10
- package/src/daemon/supervise.ts +1 -1
- package/src/files/containment.ts +5 -5
- package/src/files/files.ts +12 -12
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/meta.ts +5 -2
- package/src/instance/config.ts +21 -9
- package/src/instance/instance.ts +15 -13
- package/src/kv/store.ts +4 -3
- package/src/launcher/launcher.ts +3 -3
- package/src/mesh/mesh.ts +7 -8
- package/src/mesh/relay.ts +1 -1
- package/src/messaging/delivery.ts +4 -4
- package/src/messaging/direct.ts +2 -2
- package/src/messaging/handlers.ts +4 -4
- package/src/messaging/inbox.ts +1 -1
- package/src/messaging/notify.ts +11 -11
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +17 -17
- package/src/sessions/harness.ts +1 -1
- package/src/sessions/last-live.ts +1 -1
- package/src/sessions/registry.ts +60 -77
- package/src/sessions/search.ts +1 -1
- package/src/sessions/status.ts +7 -7
- package/src/topics/handlers.ts +2 -2
- package/src/topics/topics.ts +0 -0
- package/src/transcript/items/classify.ts +42 -37
- package/src/transcript/items/document.ts +2 -2
- package/src/transcript/items/ids.ts +0 -0
- package/src/transcript/items/render.ts +24 -24
- package/src/transcript/items/select.ts +5 -5
- package/src/transcript/transcripts.ts +3 -3
- package/src/translate/translate.ts +1 -1
- package/src/transport/driver.ts +13 -11
- package/src/upstream/events.ts +108 -10
- package/src/upstream/gateway.ts +10 -5
- package/src/upstream/requests.ts +103 -9
- package/src/upstream/status.ts +1 -1
- package/src/version.ts +1 -1
package/src/instance/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute } from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { type DumpPreset, type Endpoint, TranscriptItemSelector } from "@ccmsg/protocol";
|
|
4
4
|
import { DEFAULT_HARNESS, type Harness, HARNESSES, isHarness } from "../harness/index.ts";
|
|
5
5
|
import { parseCidr } from "./client.ts";
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ export interface LauncherTemplateConfig {
|
|
|
47
47
|
/** What the launcher may start, and where.
|
|
48
48
|
*
|
|
49
49
|
* Structured rather than a string because it is a form: the roots bound where a
|
|
50
|
-
* session may run, and the recipes are what `
|
|
50
|
+
* session may run, and the recipes are what `launcher.config.read` answers
|
|
51
51
|
* with. Present is what gives this instance the `launcher` capability. */
|
|
52
52
|
export interface LauncherConfig {
|
|
53
53
|
/** Absolute directories a session may be started in. A launch or a walk
|
|
@@ -55,7 +55,7 @@ export interface LauncherConfig {
|
|
|
55
55
|
readonly root_dirs: readonly string[];
|
|
56
56
|
/** In configured order; the first is the default recipe. */
|
|
57
57
|
readonly templates: readonly LauncherTemplateConfig[];
|
|
58
|
-
/** How deep `
|
|
58
|
+
/** How deep `dir.tree` walks when a request names no depth. */
|
|
59
59
|
readonly depth: number;
|
|
60
60
|
/** How long a launch may run before it is stopped. */
|
|
61
61
|
readonly timeout_secs: number;
|
|
@@ -98,7 +98,7 @@ export interface UpstreamConfig {
|
|
|
98
98
|
* not a property of the wire. A type name stays one to one with what a record
|
|
99
99
|
* is, and the groupings people reach for are made by naming a set of them. */
|
|
100
100
|
export interface DumpConfig {
|
|
101
|
-
/** In configured order, which is the order `
|
|
101
|
+
/** In configured order, which is the order `dump.presets.read` answers in. */
|
|
102
102
|
readonly presets: readonly DumpPreset[];
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -330,9 +330,18 @@ export function parseConfig(file: string, fields: Record<string, unknown>): Inst
|
|
|
330
330
|
};
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
/** One element of a selection
|
|
334
|
-
*
|
|
335
|
-
|
|
333
|
+
/** One element of a selection: a type name, a prefix of one, either negated
|
|
334
|
+
* with `-`, or `@name` for a preset. Taken from the contract's own schema
|
|
335
|
+
* rather than written again here, so a config file and a request are held to
|
|
336
|
+
* the one spelling. */
|
|
337
|
+
const SELECTOR = new RegExp(
|
|
338
|
+
TranscriptItemSelector.pattern ??
|
|
339
|
+
// A selector schema with no pattern would let every string through here,
|
|
340
|
+
// which is the one outcome worse than refusing the config file.
|
|
341
|
+
(() => {
|
|
342
|
+
throw new Error("the contract's item selector states no pattern");
|
|
343
|
+
})(),
|
|
344
|
+
);
|
|
336
345
|
|
|
337
346
|
function dumpOf(file: string, raw: unknown): DumpConfig {
|
|
338
347
|
if (raw === undefined) return { presets: [] };
|
|
@@ -368,9 +377,12 @@ function presetsOf(file: string, raw: unknown): DumpPreset[] {
|
|
|
368
377
|
const types = stringsOf(file, `${at}.opts.types`, opts["types"]);
|
|
369
378
|
const wrong = types.filter((element) => !SELECTOR.test(element));
|
|
370
379
|
if (wrong.length > 0) {
|
|
380
|
+
// Named rather than dropped: a selection nothing can match would dump an
|
|
381
|
+
// empty file and say why nowhere. The preset is named beside the strings
|
|
382
|
+
// so the line to edit is the one the message points at.
|
|
371
383
|
throw new ConfigError(
|
|
372
384
|
file,
|
|
373
|
-
|
|
385
|
+
`dump.presets[${name}].opts.types must be item types, prefixes, exclusions or @presets, got ${wrong.join(", ")}`,
|
|
374
386
|
);
|
|
375
387
|
}
|
|
376
388
|
return {
|
|
@@ -515,7 +527,7 @@ function upstreamOf(file: string, raw: unknown): UpstreamConfig {
|
|
|
515
527
|
};
|
|
516
528
|
}
|
|
517
529
|
|
|
518
|
-
/** How deep `
|
|
530
|
+
/** How deep `dir.tree` walks, and how long a launch may take, when the config
|
|
519
531
|
* says neither. */
|
|
520
532
|
const DEFAULT_DEPTH = 2;
|
|
521
533
|
const DEFAULT_TIMEOUT_SECS = 10;
|
package/src/instance/instance.ts
CHANGED
|
@@ -394,7 +394,7 @@ export class Instance {
|
|
|
394
394
|
publish: (topic, data, instance) => {
|
|
395
395
|
this.#topics.publish(topic, data, instance);
|
|
396
396
|
},
|
|
397
|
-
// What a peer wrote on `
|
|
397
|
+
// What a peer wrote on `auth.records`, folded into the set this instance
|
|
398
398
|
// holds. It is not relayed onward: every instance subscribes to every
|
|
399
399
|
// peer, so a record reaches all of them without anyone repeating it, and
|
|
400
400
|
// what this instance writes travels as its own (DR-0001 §2.6).
|
|
@@ -552,11 +552,11 @@ export class Instance {
|
|
|
552
552
|
this.#topics.attach("transcript", this.#transcripts);
|
|
553
553
|
// One tail feeds both: the bytes as they are appended, and what those
|
|
554
554
|
// bytes were read as.
|
|
555
|
-
this.#topics.attach("
|
|
556
|
-
this.#topics.attach("
|
|
557
|
-
this.#topics.attach("
|
|
558
|
-
this.#topics.attach("
|
|
559
|
-
this.#topics.attach("
|
|
555
|
+
this.#topics.attach("transcript.items", this.#transcripts);
|
|
556
|
+
this.#topics.attach("session.status", this.#status);
|
|
557
|
+
this.#topics.attach("session.errors", this.#status);
|
|
558
|
+
this.#topics.attach("llm.requests", this.#gateway.requests);
|
|
559
|
+
this.#topics.attach("llm.status", this.#gateway.statusResource);
|
|
560
560
|
|
|
561
561
|
// The one thing here that is written down and is nobody's derived value
|
|
562
562
|
// (§3.6): what a person saved through a client, which no other party holds
|
|
@@ -574,7 +574,7 @@ export class Instance {
|
|
|
574
574
|
self: this.self,
|
|
575
575
|
...(now === undefined ? {} : { now }),
|
|
576
576
|
publish: (written) => {
|
|
577
|
-
this.#topics.publish("
|
|
577
|
+
this.#topics.publish("auth.records", { records: written });
|
|
578
578
|
},
|
|
579
579
|
});
|
|
580
580
|
this.#auth = new Auth({
|
|
@@ -590,7 +590,7 @@ export class Instance {
|
|
|
590
590
|
this.log.write(msg, fields);
|
|
591
591
|
},
|
|
592
592
|
});
|
|
593
|
-
this.#topics.attach("
|
|
593
|
+
this.#topics.attach("auth.records", new AuthTopic(this.self, records));
|
|
594
594
|
|
|
595
595
|
// The upstreams that answer a question rather than hold a value. Each is
|
|
596
596
|
// built only where its config named one, and dispatch has already refused
|
|
@@ -618,8 +618,10 @@ export class Instance {
|
|
|
618
618
|
const origin = config.upstream.sandbox_origin;
|
|
619
619
|
|
|
620
620
|
this.#handlers = completeHandlers({
|
|
621
|
-
hello: this.#sessions.
|
|
622
|
-
|
|
621
|
+
"hello.session": this.#sessions.helloSession,
|
|
622
|
+
"hello.user": this.#sessions.helloUser,
|
|
623
|
+
"hello.instance": this.#sessions.helloInstance,
|
|
624
|
+
"session.stopping": this.#sessions.stopping,
|
|
623
625
|
...topicHandlers(this.#topics),
|
|
624
626
|
...messagingHandlers(this.#delivery, this.#notify),
|
|
625
627
|
...fileHandlers(files),
|
|
@@ -643,8 +645,8 @@ export class Instance {
|
|
|
643
645
|
...gatewayHandlers(setup),
|
|
644
646
|
...kvHandlers(kv),
|
|
645
647
|
...authHandlers(this.#auth),
|
|
646
|
-
|
|
647
|
-
|
|
648
|
+
"instance.ping": (): InstancePingResult => this.ping(),
|
|
649
|
+
"instance.shutdown": () => {
|
|
648
650
|
// The reply goes out when this handler's value reaches the driver, so
|
|
649
651
|
// stopping is deferred past that turn of the loop rather than run
|
|
650
652
|
// here — the caller is told the request was accepted, which is what
|
|
@@ -852,7 +854,7 @@ export class Instance {
|
|
|
852
854
|
}
|
|
853
855
|
// Let in as a peer rather than on the entry token, and still unproven:
|
|
854
856
|
// the greeting is the one thing it was admitted to make.
|
|
855
|
-
if (this.#mesh.unproven(conn) && opOf(frame) !== "hello") {
|
|
857
|
+
if (this.#mesh.unproven(conn) && opOf(frame) !== "hello.instance") {
|
|
856
858
|
conn.close();
|
|
857
859
|
return failure(
|
|
858
860
|
requestIdOf(frame),
|
package/src/kv/store.ts
CHANGED
|
@@ -186,10 +186,11 @@ function forget(entries: Map<string, Held>, now: Timestamp): Map<string, Held> {
|
|
|
186
186
|
|
|
187
187
|
export function kvHandlers(store: KvStore) {
|
|
188
188
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
"kv.read": (input: HandlerInput): KvReadResult =>
|
|
190
|
+
store.read(input.args as unknown as KvReadArgs),
|
|
191
|
+
"kv.write": (input: HandlerInput): KvWriteResult =>
|
|
191
192
|
store.write(input.args as unknown as KvWriteArgs),
|
|
192
|
-
|
|
193
|
+
"kv.delete": (input: HandlerInput): KvDeleteResult =>
|
|
193
194
|
store.delete(input.args as unknown as KvDeleteArgs),
|
|
194
195
|
};
|
|
195
196
|
}
|
package/src/launcher/launcher.ts
CHANGED
|
@@ -126,10 +126,10 @@ export class Launcher {
|
|
|
126
126
|
|
|
127
127
|
export function launcherHandlers(launcher: Launcher) {
|
|
128
128
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
"launcher.config.read": (): LauncherConfigReadResult => launcher.configRead(),
|
|
130
|
+
"launcher.run": (input: HandlerInput): Promise<LauncherRunResult> =>
|
|
131
131
|
launcher.run(input.args as unknown as LauncherRunArgs),
|
|
132
|
-
|
|
132
|
+
"dir.tree": (input: HandlerInput): DirTreeResult =>
|
|
133
133
|
launcher.tree(input.args as unknown as DirTreeArgs),
|
|
134
134
|
};
|
|
135
135
|
}
|
package/src/mesh/mesh.ts
CHANGED
|
@@ -122,7 +122,7 @@ export interface MeshHost {
|
|
|
122
122
|
publish(topic: string, data: unknown, instance: InstanceId): void;
|
|
123
123
|
/** Take a frame on a topic the relay does not carry.
|
|
124
124
|
*
|
|
125
|
-
* `
|
|
125
|
+
* `auth.records` is the one: its granularity is `element`, so a frame states
|
|
126
126
|
* the entries that moved rather than a whole value per instance, and there is
|
|
127
127
|
* nothing for the relay's last-value-per-instance table to hold. What
|
|
128
128
|
* receives it is the set itself, which merges by key (DR-0001 §2.6). */
|
|
@@ -469,8 +469,8 @@ export class Mesh {
|
|
|
469
469
|
|
|
470
470
|
/** Ask another instance one op, as this instance rather than for anybody.
|
|
471
471
|
*
|
|
472
|
-
* What the person's authentication needs of a peer (`
|
|
473
|
-
* `
|
|
472
|
+
* What the person's authentication needs of a peer (`auth.resolve`,
|
|
473
|
+
* `auth.rotate`) is a fact only its issuer holds, asked for by the instance
|
|
474
474
|
* that needs it — so the `caller` is this instance's own role, and the
|
|
475
475
|
* request travels the ordinary forwarding path (§7.3, DR-0001 §2.6).
|
|
476
476
|
*
|
|
@@ -537,7 +537,7 @@ export class Mesh {
|
|
|
537
537
|
* instance asks of every peer, and the frames come back unchanged (§7.4).
|
|
538
538
|
* `peers` is never given up, because it is also the routing table. */
|
|
539
539
|
demand(topic: string, wanted: boolean): void {
|
|
540
|
-
// `
|
|
540
|
+
// `auth.records` is never given up and never asked for on demand: every
|
|
541
541
|
// instance holds the whole set whether or not anything local is watching
|
|
542
542
|
// it, the way `peers` is also the routing table (§7.4, DR-0001 §2.6).
|
|
543
543
|
if (topic === AUTH_TOPIC || !isClusterTopic(topic)) return;
|
|
@@ -561,7 +561,7 @@ export class Mesh {
|
|
|
561
561
|
* the ordering is the connection's rather than a delay chosen here. */
|
|
562
562
|
#ask(conn: Requester, topic: string, wanted: boolean, afterAck = false): void {
|
|
563
563
|
const frame = {
|
|
564
|
-
op: wanted ? "
|
|
564
|
+
op: wanted ? "topic.subscribe" : "topic.unsubscribe",
|
|
565
565
|
request_id: `mesh-sub-${randomId()}`,
|
|
566
566
|
topic,
|
|
567
567
|
// The instance asks on behalf of whoever subscribed to it, and what they
|
|
@@ -569,7 +569,7 @@ export class Mesh {
|
|
|
569
569
|
// any one session: a cluster topic is the same value for all of them
|
|
570
570
|
// (§6.2), so there is nothing narrower to name.
|
|
571
571
|
//
|
|
572
|
-
// `
|
|
572
|
+
// `auth.records` is the exception, and the one topic no person may hear:
|
|
573
573
|
// it carries the tokens that authenticate them, so the instance asks for
|
|
574
574
|
// it as itself (DR-0001 §2.6).
|
|
575
575
|
caller: (topic === AUTH_TOPIC
|
|
@@ -893,9 +893,8 @@ export class Mesh {
|
|
|
893
893
|
}
|
|
894
894
|
minted.conn = conn;
|
|
895
895
|
conn.send({
|
|
896
|
-
op: "hello",
|
|
896
|
+
op: "hello.instance",
|
|
897
897
|
request_id: `mesh-hello-${key.kid}`,
|
|
898
|
-
role: "instance",
|
|
899
898
|
protocol_version: PROTOCOL_VERSION,
|
|
900
899
|
mesh: { ver: MESH_VER, iss: self, aud: peer, id: this.deps.id, kid: key.kid },
|
|
901
900
|
});
|
package/src/mesh/relay.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const CLUSTER_TOPICS: readonly string[] = [
|
|
|
35
35
|
* It is `element`-granular and its elements are the instances' own, so it is
|
|
36
36
|
* asked for as the instance rather than on a person's behalf, and folded into
|
|
37
37
|
* the set this instance holds rather than held here (DR-0001 §2.6). */
|
|
38
|
-
export const AUTH_TOPIC = "
|
|
38
|
+
export const AUTH_TOPIC = "auth.records";
|
|
39
39
|
|
|
40
40
|
export function isClusterTopic(topic: string): boolean {
|
|
41
41
|
return CLUSTER_TOPICS.includes(topic);
|
|
@@ -40,7 +40,7 @@ export interface SessionLookup {
|
|
|
40
40
|
|
|
41
41
|
/** The rest of the cluster, for a message addressed outside this instance.
|
|
42
42
|
*
|
|
43
|
-
* `
|
|
43
|
+
* `message.send` is a `cluster` op — any instance may be asked — but a message
|
|
44
44
|
* reaches a session through the session's own connections, which are held by
|
|
45
45
|
* the instance it greeted. So the op is answered here by carrying it there
|
|
46
46
|
* (§3.2 step 6 is about `instance-local` ops; this is the same forwarding for
|
|
@@ -99,7 +99,7 @@ export class Delivery implements UpstreamResource {
|
|
|
99
99
|
this.#counter = deps.inbox.lastCounter(`${deps.self}/`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/** `
|
|
102
|
+
/** `message.send`. The op fails only for a sid nobody knows; every other
|
|
103
103
|
* outcome is a success carrying what became of the message. */
|
|
104
104
|
send = async (input: HandlerInput): Promise<MessageSendResult> => {
|
|
105
105
|
const args = input.args as unknown as MessageSendArgs;
|
|
@@ -161,7 +161,7 @@ export class Delivery implements UpstreamResource {
|
|
|
161
161
|
: undefined;
|
|
162
162
|
}
|
|
163
163
|
// The sender, as the owning instance will run the op as: the identity the
|
|
164
|
-
// connection greeted with, which is the same thing `
|
|
164
|
+
// connection greeted with, which is the same thing `message.send` reads to
|
|
165
165
|
// decide who a message is from (§4.1).
|
|
166
166
|
const answer = await cluster.forward(owner, input.args, callerOf(input));
|
|
167
167
|
if (answer.kind === "reply") {
|
|
@@ -366,7 +366,7 @@ export class Delivery implements UpstreamResource {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
/** Who is asking, as another instance is told it (contract, `CallerIdentity`).
|
|
369
|
-
* A connection with no settled greeting names nobody, and `
|
|
369
|
+
* A connection with no settled greeting names nobody, and `message.send`
|
|
370
370
|
* refuses it before this is reached. */
|
|
371
371
|
function callerOf(input: HandlerInput): CallerIdentity | undefined {
|
|
372
372
|
const identity = input.identity;
|
package/src/messaging/direct.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const DIRECT_ACK_MS = 2_000;
|
|
|
59
59
|
* peer message at its inbound gate and reports the outcome from that same
|
|
60
60
|
* decision, so a receipt for a message we have finished writing is one connect
|
|
61
61
|
* and one line away on a socket of this same host. A quarter second is far
|
|
62
|
-
* more than that costs and far less than a person waits for `
|
|
62
|
+
* more than that costs and far less than a person waits for `message.send` to
|
|
63
63
|
* answer. Nothing measured stands behind the number itself. */
|
|
64
64
|
export const DIRECT_STATUS_MS = 250;
|
|
65
65
|
|
|
@@ -358,7 +358,7 @@ const DROPPED = HARNESSES.filter((harness) => harness !== "codex").flatMap((harn
|
|
|
358
358
|
* belong to the interactive interface.
|
|
359
359
|
*
|
|
360
360
|
* The budget is here for what is not being predicted: a child that never
|
|
361
|
-
* answers would hold `
|
|
361
|
+
* answers would hold `message.send` open for as long as it lived, and route
|
|
362
362
|
* (b) exists exactly so a route that does not come through costs a message
|
|
363
363
|
* nothing (§4.1). It is generous next to a call that has been measured to
|
|
364
364
|
* return at once. */
|
|
@@ -6,9 +6,9 @@ import type { Notify } from "./notify.ts";
|
|
|
6
6
|
* own entry point under the contract's name. */
|
|
7
7
|
export function messagingHandlers(delivery: Delivery, notify: Notify) {
|
|
8
8
|
return {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
"message.send": delivery.send,
|
|
10
|
+
"notify.send": notify.send,
|
|
11
|
+
"say.post": notify.post,
|
|
12
|
+
"say.unread.clear": notify.markRead,
|
|
13
13
|
};
|
|
14
14
|
}
|
package/src/messaging/inbox.ts
CHANGED
|
@@ -34,7 +34,7 @@ type Record_ =
|
|
|
34
34
|
/** What was said to a session and has not reached it.
|
|
35
35
|
*
|
|
36
36
|
* The one thing here that nothing else can reconstruct (§3.6): the sender's
|
|
37
|
-
* `
|
|
37
|
+
* `message.send` has already been answered, no transcript holds a message that
|
|
38
38
|
* was never handed over, and the text lives nowhere else. Losing this file
|
|
39
39
|
* loses the words.
|
|
40
40
|
*
|
package/src/messaging/notify.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type {
|
|
|
3
3
|
Notification,
|
|
4
4
|
NotifySendArgs,
|
|
5
5
|
NotifySendResult,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
SayUnreadClearArgs,
|
|
7
|
+
SayUnreadClearResult,
|
|
8
8
|
SayPostArgs,
|
|
9
9
|
SayPostResult,
|
|
10
10
|
Sid,
|
|
@@ -29,8 +29,8 @@ export interface NotifyDeps {
|
|
|
29
29
|
/** The `notify` topic and the three ops that speak on it.
|
|
30
30
|
*
|
|
31
31
|
* One object for all three because they are one thing seen from two sides: a
|
|
32
|
-
* line reaching a person watching. `
|
|
33
|
-
* about a session; `
|
|
32
|
+
* line reaching a person watching. `notify.send` is somebody telling a person
|
|
33
|
+
* about a session; `say.post` is a session saying that it just spoke. Both end
|
|
34
34
|
* as the same frame, which is what keeps "a notification" from meaning two
|
|
35
35
|
* shapes depending on which op raised it.
|
|
36
36
|
*
|
|
@@ -44,7 +44,7 @@ export class Notify implements UpstreamResource {
|
|
|
44
44
|
|
|
45
45
|
constructor(private readonly deps: NotifyDeps) {}
|
|
46
46
|
|
|
47
|
-
/** `
|
|
47
|
+
/** `notify.send`. The subject is the argument when it names one and the
|
|
48
48
|
* caller otherwise, so a session notifying about itself says only the text. */
|
|
49
49
|
send = (input: HandlerInput): NotifySendResult => {
|
|
50
50
|
const args = input.args as unknown as NotifySendArgs;
|
|
@@ -52,7 +52,7 @@ export class Notify implements UpstreamResource {
|
|
|
52
52
|
return {};
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
/** `
|
|
55
|
+
/** `say.post`. What was said is already in the caller's transcript, so this
|
|
56
56
|
* pushes the occurrence and raises the unread mark; the instance keeps no log
|
|
57
57
|
* of its own. The op is open to sessions alone, so the subject is the caller
|
|
58
58
|
* and there is nothing to address. */
|
|
@@ -64,9 +64,9 @@ export class Notify implements UpstreamResource {
|
|
|
64
64
|
return { posted_at };
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
/** `
|
|
68
|
-
markRead = (input: HandlerInput):
|
|
69
|
-
const { sid } = input.args as unknown as
|
|
67
|
+
/** `say.unread.clear`. One session's mark, or every one when none is named. */
|
|
68
|
+
markRead = (input: HandlerInput): SayUnreadClearResult => {
|
|
69
|
+
const { sid } = input.args as unknown as SayUnreadClearArgs;
|
|
70
70
|
if (sid === undefined) this.#unread.clear();
|
|
71
71
|
else this.#unread.delete(sid);
|
|
72
72
|
return {};
|
|
@@ -74,7 +74,7 @@ export class Notify implements UpstreamResource {
|
|
|
74
74
|
|
|
75
75
|
/** The sessions that have spoken unheard. Nothing in this generation of the
|
|
76
76
|
* contract carries the mark on the wire, so this is how the instance's own
|
|
77
|
-
* side reads what `
|
|
77
|
+
* side reads what `say.unread.clear` clears. */
|
|
78
78
|
unread(): readonly Sid[] {
|
|
79
79
|
return [...this.#unread];
|
|
80
80
|
}
|
|
@@ -113,7 +113,7 @@ export class Notify implements UpstreamResource {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/** The session the caller is. A person's connection names none, which is why
|
|
116
|
-
* `
|
|
116
|
+
* `notify.send` takes the subject as an argument — one that omits it from a
|
|
117
117
|
* connection with no session has named nobody for the notification to be
|
|
118
118
|
* about. */
|
|
119
119
|
#caller(input: HandlerInput): Sid {
|
package/src/sessions/dump.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface DumpDeps {
|
|
|
47
47
|
*
|
|
48
48
|
* The subject is the session, or one agent below it when the request names
|
|
49
49
|
* one. Every item type is read from wherever the subject stands — an agent's
|
|
50
|
-
* `message
|
|
50
|
+
* `message.user.in` is the brief its parent gave it — so one selection carries
|
|
51
51
|
* unchanged down a chain of agents, which is what makes the ledger's agent ids
|
|
52
52
|
* a way to descend rather than just a list. */
|
|
53
53
|
export function dumpWrite(args: SessionDumpWriteArgs, deps: DumpDeps): SessionDumpWriteResult {
|
package/src/sessions/handlers.ts
CHANGED
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
type SessionDumpWriteArgs,
|
|
7
7
|
type SessionEnvReadArgs,
|
|
8
8
|
type SessionEnvReadResult,
|
|
9
|
-
type
|
|
10
|
-
type
|
|
9
|
+
type SessionForkOriginReadArgs,
|
|
10
|
+
type SessionForkOriginReadResult,
|
|
11
11
|
type SessionKillArgs,
|
|
12
12
|
type SessionKillResult,
|
|
13
|
-
type
|
|
14
|
-
type
|
|
13
|
+
type SessionForgetArgs,
|
|
14
|
+
type SessionForgetResult,
|
|
15
15
|
type SessionRenameArgs,
|
|
16
16
|
type SessionRenameResult,
|
|
17
17
|
type SessionSearchArgs,
|
|
@@ -69,7 +69,7 @@ export interface SessionOpsDeps {
|
|
|
69
69
|
/** The ops that observe and operate on sessions.
|
|
70
70
|
*
|
|
71
71
|
* None of them decides who may call it: dispatch has settled that from the
|
|
72
|
-
* attribute table. The one that narrows by role is `
|
|
72
|
+
* attribute table. The one that narrows by role is `transcript.read`, and it
|
|
73
73
|
* narrows through the same `sees` the file ops narrow through — the visible
|
|
74
74
|
* range of a `scope: "role"` op is one rule, in one place, whatever it is
|
|
75
75
|
* a range over. */
|
|
@@ -77,12 +77,12 @@ export function sessionHandlers(deps: SessionOpsDeps) {
|
|
|
77
77
|
const viewer = (input: HandlerInput): Viewer => ({ role: input.role, sid: input.identity?.sid });
|
|
78
78
|
|
|
79
79
|
return {
|
|
80
|
-
|
|
80
|
+
"session.kill": async (input: HandlerInput): Promise<SessionKillResult> => {
|
|
81
81
|
const args = input.args as unknown as SessionKillArgs;
|
|
82
82
|
return await deps.processes.kill(args.sid, args.force === true);
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
"session.rename": async (input: HandlerInput): Promise<SessionRenameResult> => {
|
|
86
86
|
const args = input.args as unknown as SessionRenameArgs;
|
|
87
87
|
const title = validTitle(args.title);
|
|
88
88
|
const terminal = await deps.processes.terminal(args.sid);
|
|
@@ -93,20 +93,20 @@ export function sessionHandlers(deps: SessionOpsDeps) {
|
|
|
93
93
|
return { terminal_id: terminal.id, instance: deps.self, title };
|
|
94
94
|
},
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
"session.env.read": async (input: HandlerInput): Promise<SessionEnvReadResult> => {
|
|
97
97
|
const args = input.args as unknown as SessionEnvReadArgs;
|
|
98
98
|
const { pid, env } = await deps.processes.environment(args.sid);
|
|
99
99
|
return { pid, instance: deps.self, env };
|
|
100
100
|
},
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
"session.search": (input: HandlerInput) =>
|
|
103
103
|
search(input.args as unknown as SessionSearchArgs, {
|
|
104
104
|
self: deps.self,
|
|
105
105
|
configHome: deps.configHome,
|
|
106
106
|
files: deps.files,
|
|
107
107
|
}),
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
"session.dump.write": (input: HandlerInput) =>
|
|
110
110
|
dumpWrite(input.args as unknown as SessionDumpWriteArgs, {
|
|
111
111
|
self: deps.self,
|
|
112
112
|
stateDir: deps.stateDir,
|
|
@@ -120,22 +120,22 @@ export function sessionHandlers(deps: SessionOpsDeps) {
|
|
|
120
120
|
* free-text field and let the instance refuse. A preset that references
|
|
121
121
|
* another is answered as written: the expansion, and the refusal of a
|
|
122
122
|
* cycle, happen where the config is read. */
|
|
123
|
-
|
|
123
|
+
"dump.presets.read": (): DumpPresetsReadResult => ({ presets: [...deps.presets] }),
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
const args = input.args as unknown as
|
|
125
|
+
"session.fork.origin.read": (input: HandlerInput): SessionForkOriginReadResult => {
|
|
126
|
+
const args = input.args as unknown as SessionForkOriginReadArgs;
|
|
127
127
|
const origin = forkOrigin(args.sid, deps.files);
|
|
128
128
|
return origin === undefined ? {} : { origin };
|
|
129
129
|
},
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
const args = input.args as unknown as
|
|
131
|
+
"session.forget": (input: HandlerInput): SessionForgetResult => {
|
|
132
|
+
const args = input.args as unknown as SessionForgetArgs;
|
|
133
133
|
// An unknown session is not an error: two clients pressing the same
|
|
134
134
|
// button is the ordinary case, and the caller's goal holds either way.
|
|
135
135
|
return { removed: deps.forget(args.sid) };
|
|
136
136
|
},
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
"transcript.read": (input: HandlerInput): TranscriptReadResult => {
|
|
139
139
|
const args = input.args as unknown as TranscriptReadArgs;
|
|
140
140
|
if (!sees(args.sid, viewer(input))) {
|
|
141
141
|
// The role sets the visible range, not the permission (§3.2): outside
|
|
@@ -153,7 +153,7 @@ export function sessionHandlers(deps: SessionOpsDeps) {
|
|
|
153
153
|
* The role narrows it the way it narrows the raw read: what a role may see
|
|
154
154
|
* is one rule whatever is being read, and a caller that cannot see a
|
|
155
155
|
* session cannot see it in either vocabulary. */
|
|
156
|
-
|
|
156
|
+
"transcript.items.read": (input: HandlerInput): TranscriptItemsReadResult => {
|
|
157
157
|
const args = input.args as unknown as TranscriptItemsReadArgs;
|
|
158
158
|
if (!sees(args.sid, viewer(input))) {
|
|
159
159
|
throw new OpError("not_found", `no transcript is known for ${args.sid}`);
|
package/src/sessions/harness.ts
CHANGED
|
@@ -164,7 +164,7 @@ export class HarnessSessions implements OwnSessions {
|
|
|
164
164
|
*
|
|
165
165
|
* Every answer comes from here rather than from anything the watch left
|
|
166
166
|
* behind. Which sessions exist is an input to the classification (§5.1), and
|
|
167
|
-
* classifying happens inside `
|
|
167
|
+
* classifying happens inside `message.send`'s decision and inside the
|
|
168
168
|
* recompute that writes `last_live` — neither of which can hand back a
|
|
169
169
|
* promise without changing what it means, and neither of which may depend on
|
|
170
170
|
* somebody being subscribed. The ops that signal a session's process read it
|
|
@@ -106,7 +106,7 @@ export class LastLiveStore {
|
|
|
106
106
|
this.#save();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** Drop one entry: `
|
|
109
|
+
/** Drop one entry: `session.forget`, and a session registering
|
|
110
110
|
* again, which is what moves it back to the connected list. */
|
|
111
111
|
remove(sid: Sid): boolean {
|
|
112
112
|
if (!this.#entries.delete(sid)) return false;
|