@cordfuse/crosstalk 6.0.0-alpha.7 → 6.0.0-alpha.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordfuse/crosstalk",
3
- "version": "6.0.0-alpha.7",
3
+ "version": "6.0.0-alpha.9",
4
4
  "description": "Crosstalk runtime — async messaging between agents over git, across machines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/actor.ts CHANGED
@@ -1,8 +1,31 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { join } from 'path';
3
- import { hostname as osHostname } from 'os';
3
+ import { hostname as osHostname, platform } from 'os';
4
+ import { spawnSync } from 'child_process';
4
5
  import { parseFrontmatter } from './frontmatter.js';
5
6
 
7
+ // Collect the names this machine might be known by. On macOS, the kernel
8
+ // hostname (`os.hostname()`) drifts with DHCP/VPN/Tailscale when the static
9
+ // HostName is unset — `scutil --get LocalHostName` is the stable Bonjour
10
+ // name (e.g. `Steves-MacBook-Air`), and host files commonly use the `.local`
11
+ // form. Trying all variants makes auto-detect deterministic across network
12
+ // state without forcing every Mac operator to pin `--host`.
13
+ function candidateHostNames(): string[] {
14
+ const names = new Set<string>();
15
+ names.add(osHostname());
16
+ if (platform() === 'darwin') {
17
+ const r = spawnSync('scutil', ['--get', 'LocalHostName'], { encoding: 'utf-8' });
18
+ if (r.status === 0) {
19
+ const local = r.stdout.trim();
20
+ if (local) {
21
+ names.add(local);
22
+ names.add(`${local}.local`);
23
+ }
24
+ }
25
+ }
26
+ return [...names];
27
+ }
28
+
6
29
  export interface HostActorTier {
7
30
  cli: string;
8
31
  count?: number;
@@ -36,13 +59,15 @@ export function findHostFile(transportRoot: string, override?: string): HostFile
36
59
  if (!target) throw new Error(`Host file '${override}' not found in ${dir}`);
37
60
  return parseHostFile(join(dir, target));
38
61
  }
39
- const hostName = osHostname();
62
+ const names = candidateHostNames();
40
63
  for (const f of files) {
41
64
  const parsed = parseHostFile(join(dir, f));
42
- if (parsed.hostname === hostName || parsed.alias === hostName) return parsed;
65
+ for (const n of names) {
66
+ if (parsed.hostname === n || parsed.alias === n) return parsed;
67
+ }
43
68
  }
44
69
  throw new Error(
45
- `No host file matches hostname '${hostName}' in ${dir}. ` +
70
+ `No host file matches any of [${names.join(', ')}] in ${dir}. ` +
46
71
  `Pass --host <alias> to override.`,
47
72
  );
48
73
  }
package/src/dispatch.ts CHANGED
@@ -362,17 +362,23 @@ async function dispatchTick(): Promise<TickResult> {
362
362
  const pending: PendingDispatch[] = [];
363
363
 
364
364
  for (const channelUuid of channels) {
365
- const cursor = readCursor(transportRoot, actorName, channelUuid);
366
- if (cursor === head) continue;
365
+ const persistedCursor = readCursor(transportRoot, actorName, channelUuid);
366
+ if (persistedCursor === head) continue;
367
367
 
368
368
  // First encounter: seed to the commit that introduced this actor's host
369
369
  // file. Messages sent after the host joined are delivered (store-and-
370
370
  // forward); pre-join history is ignored. Seeding to HEAD would silently
371
371
  // drop messages sent while the dispatcher was offline — the wrong trade.
372
- if (cursor === null) {
372
+ // Fall through after seeding so this tick processes the post-join backlog
373
+ // (otherwise `--once` users hit a seed-then-dispatch two-tick gotcha).
374
+ let cursor: string;
375
+ if (persistedCursor === null) {
373
376
  const joinCommit = hostFileCommit(transportRoot, host.alias);
374
- writeCursor(transportRoot, actorName, channelUuid, joinCommit ?? head);
375
- continue;
377
+ cursor = joinCommit ?? head;
378
+ writeCursor(transportRoot, actorName, channelUuid, cursor);
379
+ if (cursor === head) continue;
380
+ } else {
381
+ cursor = persistedCursor;
376
382
  }
377
383
 
378
384
  const messages = listChannelMessages(transportRoot, channelUuid);
package/src/send.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // Operators (no env) always send new tasks.
9
9
 
10
10
  import { resolve, join } from 'path';
11
- import { mkdirSync, writeFileSync } from 'fs';
11
+ import { mkdirSync, writeFileSync, existsSync } from 'fs';
12
12
  import { now, messageFilename } from './filenames.js';
13
13
  import { serializeFrontmatter } from './frontmatter.js';
14
14
  import { gitCommitAndPush, discoverChannels } from './transport.js';
@@ -56,6 +56,26 @@ async function main(): Promise<void> {
56
56
  }
57
57
  }
58
58
 
59
+ // Warn when --to names an actor@host whose host file isn't in the transport
60
+ // yet. The dispatcher's high-water-mark seeds past any commit before the
61
+ // host file landed, so messages to an un-provisioned host are silently
62
+ // skipped on first boot. Surfacing this here turns the silent drop into an
63
+ // operator-visible error before the message is even written.
64
+ const recipients = to.split(',').map((s) => s.trim()).filter(Boolean);
65
+ for (const recipient of recipients) {
66
+ const at = recipient.indexOf('@');
67
+ if (at === -1) continue;
68
+ const host = recipient.slice(at + 1);
69
+ const hostPath = join(transportRoot, 'hosts', `${host}.md`);
70
+ if (!existsSync(hostPath)) {
71
+ console.error(
72
+ `crosstalk send: WARNING — no host file at hosts/${host}.md. ` +
73
+ `Messages addressed to '${recipient}' will be skipped by that host ` +
74
+ `until hosts/${host}.md is committed to the transport.`,
75
+ );
76
+ }
77
+ }
78
+
59
79
  const reTargets = (isNew ? '' : process.env['CROSSTALK_DISPATCH_RE'] ?? '')
60
80
  .split(',')
61
81
  .map((s) => s.trim())