@kybernesis/dispatch 0.6.1 → 0.6.3

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/dist/channel.js CHANGED
@@ -1,4 +1,5 @@
1
- import { eveChannel } from "eve/channels/eve";
1
+ import { defaultEveAuth, eveChannel } from "eve/channels/eve";
2
+ import { recordCurrentSession } from "./current-session.js";
2
3
  import { localDev, placeholderAuth, vercelOidc, vercelSubject, } from "eve/channels/auth";
3
4
  /** Compose the OIDC subject string for a peer. Exposed for doctor checks and tests. */
4
5
  export function peerSubject(peer) {
@@ -58,6 +59,28 @@ export function dispatchChannel(options) {
58
59
  return governedAuth ? governedAuth(request) : null;
59
60
  };
60
61
  return eveChannel({
62
+ // Remember which conversation this agent is in.
63
+ //
64
+ // A schedule is handed `{ to, waitUntil, appAuth }` and has no way to name
65
+ // an existing session, so a routine cannot answer into the chat the person
66
+ // is actually looking at. This is the only place that knows — every inbound
67
+ // turn carries its session id — so it is written down here for the routine
68
+ // to find. See ./current-session.ts for why it is a file.
69
+ //
70
+ // `defaultEveAuth` is returned unchanged: this observes, it must never
71
+ // change who a turn runs as.
72
+ onMessage: (ctx) => {
73
+ const id = ctx.eve.sessionId;
74
+ if (typeof id === "string" && id !== "") {
75
+ const principal = ctx.eve.caller?.principalId;
76
+ recordCurrentSession({
77
+ sessionId: id,
78
+ ...(typeof principal === "string" ? { principalId: principal } : {}),
79
+ at: new Date().toISOString(),
80
+ });
81
+ }
82
+ return { auth: defaultEveAuth(ctx) };
83
+ },
61
84
  auth: [
62
85
  // Verifies which deployment is calling: this project's own tokens are
63
86
  // always accepted; other projects only when enumerated as peers.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The conversation this agent is currently having with a person.
3
+ *
4
+ * A schedule cannot find this out for itself. It is handed `{ to, waitUntil,
5
+ * appAuth }` and nothing else — no session list, no way to address a session by
6
+ * id — so a routine that wants to answer *into the chat the person is actually
7
+ * looking at* has no way to name it. The information exists, but only here: the
8
+ * eve channel sees `sessionId` on every inbound turn.
9
+ *
10
+ * So the channel writes it down. This is deliberately a file and not memory:
11
+ * `@kybernesis/manage` restarts the agent whenever a routine is added, and an
12
+ * in-process variable would be empty exactly when the first routine fires.
13
+ *
14
+ * Agent-local by design. The control plane has no part in this — a routine is
15
+ * the agent's own business, and routing one through a hosted service would make
16
+ * a local conversation depend on a network it never needed.
17
+ */
18
+ export interface CurrentSession {
19
+ /** The durable eve session id of the most recent inbound turn. */
20
+ sessionId: string;
21
+ /** Who was talking, when a verified principal was present. */
22
+ principalId?: string;
23
+ /** ISO timestamp, so a stale pointer is recognisable as stale. */
24
+ at: string;
25
+ }
26
+ /** Where the pointer lives, next to the agent's other runtime state. */
27
+ export declare function currentSessionFile(appRoot?: string): string;
28
+ /**
29
+ * Record the session this turn belongs to. Never throws: a read-only or full
30
+ * filesystem must not take a conversation down, and the cost of failing is one
31
+ * routine landing in a new thread instead of the open one.
32
+ */
33
+ export declare function recordCurrentSession(input: CurrentSession, appRoot?: string): void;
34
+ /** Read the pointer, or undefined when there has never been a turn. */
35
+ export declare function readCurrentSession(appRoot?: string): CurrentSession | undefined;
@@ -0,0 +1,45 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ /** Where the pointer lives, next to the agent's other runtime state. */
4
+ export function currentSessionFile(appRoot) {
5
+ const root = appRoot ?? process.env.EVE_APP_DIR ?? process.cwd();
6
+ return join(root, ".eve", "current-session.json");
7
+ }
8
+ /**
9
+ * Record the session this turn belongs to. Never throws: a read-only or full
10
+ * filesystem must not take a conversation down, and the cost of failing is one
11
+ * routine landing in a new thread instead of the open one.
12
+ */
13
+ export function recordCurrentSession(input, appRoot) {
14
+ try {
15
+ const file = currentSessionFile(appRoot);
16
+ mkdirSync(dirname(file), { recursive: true });
17
+ // Written to a sibling and renamed: a reader that arrives mid-write gets
18
+ // the old pointer rather than half a JSON document.
19
+ const tmp = `${file}.${process.pid}.tmp`;
20
+ writeFileSync(tmp, JSON.stringify(input), "utf8");
21
+ renameSync(tmp, file);
22
+ }
23
+ catch {
24
+ // Intentionally silent, see above.
25
+ }
26
+ }
27
+ /** Read the pointer, or undefined when there has never been a turn. */
28
+ export function readCurrentSession(appRoot) {
29
+ try {
30
+ const raw = JSON.parse(readFileSync(currentSessionFile(appRoot), "utf8"));
31
+ if (typeof raw !== "object" || raw === null)
32
+ return undefined;
33
+ const o = raw;
34
+ if (typeof o.sessionId !== "string" || o.sessionId === "")
35
+ return undefined;
36
+ return {
37
+ sessionId: o.sessionId,
38
+ ...(typeof o.principalId === "string" ? { principalId: o.principalId } : {}),
39
+ at: typeof o.at === "string" ? o.at : new Date(0).toISOString(),
40
+ };
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
package/dist/index.d.ts CHANGED
@@ -25,3 +25,4 @@ export { remotePeer, type RemotePeerOptions } from "./remote-peer.js";
25
25
  export { type GovernedOptions } from "./governed.js";
26
26
  export { dispatchChannel, peerSubject, type DispatchChannelOptions, type PeerRef, } from "./channel.js";
27
27
  export { governedPeers, type GovernedPeersOptions } from "./governed-peers.js";
28
+ export { currentSessionFile, readCurrentSession, recordCurrentSession, type CurrentSession, } from "./current-session.js";
package/dist/index.js CHANGED
@@ -24,3 +24,4 @@
24
24
  export { remotePeer } from "./remote-peer.js";
25
25
  export { dispatchChannel, peerSubject, } from "./channel.js";
26
26
  export { governedPeers } from "./governed-peers.js";
27
+ export { currentSessionFile, readCurrentSession, recordCurrentSession, } from "./current-session.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/dispatch",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Agent-to-agent dispatch for eve agents: declared remote-peer edges with principal forwarding on by default, and a receiver channel that makes trusting the wrong caller hard to express.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,12 +33,12 @@
33
33
  "devDependencies": {
34
34
  "@kybernesis/enterprise": "^0.8.0",
35
35
  "@types/node": "24.x",
36
- "eve": "^0.53.1",
36
+ "eve": "^0.51.1",
37
37
  "typescript": "7.0.2"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@kybernesis/enterprise": ">=0.2.0",
41
- "eve": ">=0.53.0 <0.54.0"
41
+ "eve": ">=0.51.0 <0.52.0"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=20"