@ccmsg/cli 0.2.11 → 0.2.12
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/instance/instance.ts +14 -10
- package/src/sessions/registry.ts +48 -15
- package/src/transcript/files.ts +16 -3
- package/src/transcript/transcripts.ts +4 -2
package/package.json
CHANGED
package/src/instance/instance.ts
CHANGED
|
@@ -413,13 +413,23 @@ export class Instance {
|
|
|
413
413
|
},
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
+
// Which transcript a sid means. Only this instance's config home is ever
|
|
417
|
+
// looked in (M6): a session that greeted said where its transcript is, and
|
|
418
|
+
// one that never greeted is looked for under that home and nowhere else.
|
|
419
|
+
// Both the ops that read a transcript and the tails that follow one ask
|
|
420
|
+
// here, so a sid resolves to the same file whichever way it is reached.
|
|
421
|
+
const transcriptFiles = new TranscriptFiles({
|
|
422
|
+
configHome: paths.configHome,
|
|
423
|
+
announced: (sid) => this.#sessions.transcriptPath(sid),
|
|
424
|
+
});
|
|
425
|
+
|
|
416
426
|
// The transcript tails and their folds. Built before the sessions domain
|
|
417
427
|
// and reading from it lazily: the fold is one of the sessions domain's
|
|
418
428
|
// inputs (§5.1) while the path to follow is one of its outputs, and the
|
|
419
429
|
// two meet at the moment a tail starts rather than at construction.
|
|
420
430
|
this.#transcripts = new Transcripts({
|
|
421
431
|
self: this.self,
|
|
422
|
-
pathOf: (sid) =>
|
|
432
|
+
pathOf: (sid) => transcriptFiles.path(sid),
|
|
423
433
|
publish: (topic, data) => {
|
|
424
434
|
this.#topics.publish(topic, data);
|
|
425
435
|
},
|
|
@@ -445,6 +455,9 @@ export class Instance {
|
|
|
445
455
|
transcript: this.#transcripts,
|
|
446
456
|
gateway: this.#gateway,
|
|
447
457
|
terminals: hostTerminalReader(),
|
|
458
|
+
log: (msg, fields) => {
|
|
459
|
+
this.log.write(msg, fields);
|
|
460
|
+
},
|
|
448
461
|
...(this.#mesh === undefined ? {} : { mesh: this.#mesh }),
|
|
449
462
|
onChanged: () => {
|
|
450
463
|
this.#status.refresh();
|
|
@@ -569,15 +582,6 @@ export class Instance {
|
|
|
569
582
|
});
|
|
570
583
|
const origin = config.upstream.sandbox_origin;
|
|
571
584
|
|
|
572
|
-
// Which transcript an op means, for the ops that read one rather than
|
|
573
|
-
// follow one. Only this instance's config home is ever looked in (M6): a
|
|
574
|
-
// session that greeted said where its transcript is, and one that never
|
|
575
|
-
// greeted is looked for under that home and nowhere else.
|
|
576
|
-
const transcriptFiles = new TranscriptFiles({
|
|
577
|
-
configHome: paths.configHome,
|
|
578
|
-
announced: (sid) => this.#sessions.transcriptPath(sid),
|
|
579
|
-
});
|
|
580
|
-
|
|
581
585
|
this.#handlers = completeHandlers({
|
|
582
586
|
hello: this.#sessions.hello,
|
|
583
587
|
session_stopping: this.#sessions.stopping,
|
package/src/sessions/registry.ts
CHANGED
|
@@ -67,6 +67,13 @@ export interface SessionsDeps {
|
|
|
67
67
|
readonly onChanged?: () => void;
|
|
68
68
|
/** How often the confirmation poll runs, for a test that cannot wait. */
|
|
69
69
|
readonly pollMs?: number;
|
|
70
|
+
/** Where this domain says what it declined to act on. A greeting whose
|
|
71
|
+
* `transcript_path` this instance will not read is answered `ok` all the
|
|
72
|
+
* same — the field is simply absent from what `peers` says of the session —
|
|
73
|
+
* and the reason it is absent is operational rather than contractual, so it
|
|
74
|
+
* is written here for `ccmsg daemon log` to answer with. Absent where
|
|
75
|
+
* nothing collects it. */
|
|
76
|
+
readonly log?: (message: string, fields?: Record<string, unknown>) => void;
|
|
70
77
|
/** How the terminal a session runs in is read from its process. Absent on a
|
|
71
78
|
* host where no process's environment can be read, where every row's
|
|
72
79
|
* terminal stays unknown — which is a state the classification has. */
|
|
@@ -245,7 +252,7 @@ export class Sessions implements UpstreamResource {
|
|
|
245
252
|
const expiresAt = this.deps.authExpiresAt?.(input.conn);
|
|
246
253
|
const sid = requiredSid(args);
|
|
247
254
|
if (sid !== undefined) {
|
|
248
|
-
this.register(sid, args
|
|
255
|
+
this.register(sid, args);
|
|
249
256
|
input.conn.onClose(() => this.release(sid));
|
|
250
257
|
}
|
|
251
258
|
return {
|
|
@@ -503,10 +510,15 @@ export class Sessions implements UpstreamResource {
|
|
|
503
510
|
* silence for a retraction would let each of them erase what the last one
|
|
504
511
|
* knew, and the session would be described by whichever process spoke most
|
|
505
512
|
* recently rather than by everything it has said. */
|
|
506
|
-
private register(sid: Sid, args: HelloArgs
|
|
513
|
+
private register(sid: Sid, args: HelloArgs): void {
|
|
507
514
|
const now = Date.now();
|
|
508
515
|
const held = this.#connected.get(sid);
|
|
509
|
-
const meta = {
|
|
516
|
+
const meta = {
|
|
517
|
+
...this.#stated.get(sid),
|
|
518
|
+
...metaOf(args, this.deps.configHome, (refused) => {
|
|
519
|
+
this.deps.log?.("transcript_path not taken", { sid, path: args.transcript_path, refused });
|
|
520
|
+
}),
|
|
521
|
+
};
|
|
510
522
|
this.#connected.set(sid, {
|
|
511
523
|
sid,
|
|
512
524
|
connected_at: held?.connected_at ?? now,
|
|
@@ -689,15 +701,22 @@ export class Sessions implements UpstreamResource {
|
|
|
689
701
|
* else — a session naming a file elsewhere is a session that named nothing,
|
|
690
702
|
* which is what a session that stayed silent already is (M6). It is not an
|
|
691
703
|
* error: how a session describes itself is its own business, and the instance
|
|
692
|
-
* simply does not act on a description it cannot stand behind.
|
|
693
|
-
|
|
704
|
+
* simply does not act on a description it cannot stand behind. Why a path was
|
|
705
|
+
* not taken is told to `refused`, which is the operator's answer to a field
|
|
706
|
+
* that is simply absent from what `peers` says. */
|
|
707
|
+
function metaOf(
|
|
708
|
+
args: HelloArgs,
|
|
709
|
+
configHome: string,
|
|
710
|
+
refused: (reason: string) => void,
|
|
711
|
+
): SessionMeta {
|
|
694
712
|
const meta: Record<string, string> = {};
|
|
695
713
|
for (const field of META_FIELDS) {
|
|
696
714
|
const value = args[field];
|
|
697
715
|
if (value === undefined) continue;
|
|
698
716
|
if (field === "transcript_path") {
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
717
|
+
const taken = ownTranscript(value, configHome);
|
|
718
|
+
if (typeof taken === "string") meta[field] = taken;
|
|
719
|
+
else refused(taken.refused);
|
|
701
720
|
continue;
|
|
702
721
|
}
|
|
703
722
|
meta[field] = value;
|
|
@@ -721,19 +740,33 @@ function metaOf(args: HelloArgs, configHome: string): SessionMeta {
|
|
|
721
740
|
* symlink and one spelled directly are the same path, and a link anywhere
|
|
722
741
|
* along it that leads out of the tree lands outside and is refused. What is
|
|
723
742
|
* already there must be a file: a directory by that name is not a transcript.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
743
|
+
*
|
|
744
|
+
* `projects/` itself is settled the same way, so a config home whose first
|
|
745
|
+
* session has yet to write anything is a boundary all the same: the directory
|
|
746
|
+
* that is there is followed, the part that is not is taken as spelled, and the
|
|
747
|
+
* comparison is between two paths resolved by one rule. The config home is not
|
|
748
|
+
* treated that way — an instance answers for a home it is running out of, and
|
|
749
|
+
* one that is not there names no tree to be inside of. */
|
|
750
|
+
function ownTranscript(named: string, configHome: string): string | Refused {
|
|
751
|
+
if (!isAbsolute(named)) return { refused: "not an absolute path" };
|
|
752
|
+
let projects: string | undefined;
|
|
728
753
|
try {
|
|
729
|
-
projects =
|
|
754
|
+
projects = resolveAsFarAsItGoes(join(realpathSync(configHome), "projects"));
|
|
730
755
|
} catch {
|
|
731
|
-
return
|
|
756
|
+
return { refused: "the config home is not there" };
|
|
732
757
|
}
|
|
733
758
|
const settled = resolveAsFarAsItGoes(named);
|
|
734
|
-
if (settled === undefined || !within(settled, projects))
|
|
759
|
+
if (projects === undefined || settled === undefined || !within(settled, projects)) {
|
|
760
|
+
return { refused: "outside this config home's projects tree" };
|
|
761
|
+
}
|
|
735
762
|
const stat = statSync(settled, { throwIfNoEntry: false });
|
|
736
|
-
|
|
763
|
+
if (stat !== undefined && !stat.isFile()) return { refused: "not a file" };
|
|
764
|
+
return settled;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Why a stated path was not taken, in the words the log states it in. */
|
|
768
|
+
interface Refused {
|
|
769
|
+
readonly refused: string;
|
|
737
770
|
}
|
|
738
771
|
|
|
739
772
|
/** The path with every segment of it that exists resolved.
|
package/src/transcript/files.ts
CHANGED
|
@@ -38,11 +38,24 @@ export interface TranscriptFilesDeps {
|
|
|
38
38
|
export class TranscriptFiles {
|
|
39
39
|
constructor(private readonly deps: TranscriptFilesDeps) {}
|
|
40
40
|
|
|
41
|
-
/** The session's own transcript.
|
|
42
|
-
|
|
41
|
+
/** The session's own transcript, or nothing where this instance holds none.
|
|
42
|
+
*
|
|
43
|
+
* Two ways to the one file, in the order of what each is good for: what the
|
|
44
|
+
* session announced is exact and costs no search, and the walk finds the
|
|
45
|
+
* file by the identity it carries in its name (`<sid>.jsonl`) for a session
|
|
46
|
+
* that never greeted or is no longer running. Both stay inside this
|
|
47
|
+
* instance's `projects/` — the announced path because it was taken only if
|
|
48
|
+
* it was inside it, the walk because that tree is what it walks (M6). */
|
|
49
|
+
path(sid: Sid): string | undefined {
|
|
43
50
|
const announced = this.deps.announced(sid);
|
|
44
51
|
if (announced !== undefined && isFile(announced)) return announced;
|
|
45
|
-
|
|
52
|
+
return this.find(sid);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The session's own transcript, for an op that has nothing to answer
|
|
56
|
+
* without one. */
|
|
57
|
+
session(sid: Sid): string {
|
|
58
|
+
const found = this.path(sid);
|
|
46
59
|
if (found === undefined) throw new OpError("not_found", `no transcript is held for ${sid}`);
|
|
47
60
|
return found;
|
|
48
61
|
}
|
|
@@ -5,8 +5,10 @@ import { type Appended, TranscriptTail } from "./tail.ts";
|
|
|
5
5
|
|
|
6
6
|
export interface TranscriptsDeps {
|
|
7
7
|
readonly self: InstanceId;
|
|
8
|
-
/** Where a session's transcript is
|
|
9
|
-
*
|
|
8
|
+
/** Where a session's transcript is: what it announced when it greeted
|
|
9
|
+
* (§5.1), or the `<sid>.jsonl` under this instance's `projects/` that
|
|
10
|
+
* carries its name. A sid neither names nor is named by a file there has
|
|
11
|
+
* none, and nothing is guessed for it. */
|
|
10
12
|
readonly pathOf: (sid: Sid) => string | undefined;
|
|
11
13
|
/** The one way a value reaches subscribers (§6.1). */
|
|
12
14
|
readonly publish: (topic: string, data: unknown) => void;
|