@coffer-org/server 7.0.0 → 7.1.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.
@@ -14,11 +14,11 @@ export declare class OpenAIEmbeddingError extends Error {
14
14
  });
15
15
  }
16
16
  export declare function embeddingFailureMessage(error: unknown): string;
17
- export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch): Promise<{
17
+ export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<{
18
18
  vectors: number[][];
19
19
  tokens: number;
20
20
  }>;
21
- export declare function embedOne(text: string, apiKey: string, fetchImpl?: typeof fetch): Promise<{
21
+ export declare function embedOne(text: string, apiKey: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<{
22
22
  vector: number[];
23
23
  tokens: number;
24
24
  }>;
@@ -54,7 +54,7 @@ export function embeddingFailureMessage(error) {
54
54
  return `OpenAI embeddings are unavailable${error.status == null ? '' : ` (HTTP ${error.status})`}. RAG will retry; records are still saved.`;
55
55
  }
56
56
  }
57
- export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
57
+ export async function embedBatch(texts, apiKey, fetchImpl = fetch, signal) {
58
58
  if (texts.length === 0)
59
59
  return { vectors: [], tokens: 0 };
60
60
  if (!apiKey)
@@ -65,6 +65,7 @@ export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
65
65
  method: 'POST',
66
66
  headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
67
67
  body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
68
+ ...(signal ? { signal } : {}),
68
69
  });
69
70
  }
70
71
  catch (cause) {
@@ -79,8 +80,8 @@ export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
79
80
  const vectors = [...json.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
80
81
  return { vectors, tokens: json.usage?.total_tokens ?? 0 };
81
82
  }
82
- export async function embedOne(text, apiKey, fetchImpl = fetch) {
83
- const { vectors, tokens } = await embedBatch([text], apiKey, fetchImpl);
83
+ export async function embedOne(text, apiKey, fetchImpl = fetch, signal) {
84
+ const { vectors, tokens } = await embedBatch([text], apiKey, fetchImpl, signal);
84
85
  const vector = vectors[0];
85
86
  if (!vector)
86
87
  throw new Error('embeddings: empty response');
@@ -39,3 +39,4 @@ export declare function searchEmbeddings(queryVec: number[], k: number, opts?: {
39
39
  shelfKeys?: Set<string>;
40
40
  }): Promise<EmbeddingHit[]>;
41
41
  export declare function listEventsSince(lastId: number, limit: number): Promise<EventRow[]>;
42
+ export declare function latestEventId(): Promise<number>;
@@ -116,3 +116,11 @@ export async function listEventsSince(lastId, limit) {
116
116
  }));
117
117
  return rows;
118
118
  }
119
+ export async function latestEventId() {
120
+ const em = getEm().fork();
121
+ const rows = (await em.find('_Event', {}, {
122
+ orderBy: { id: 'DESC' },
123
+ limit: 1,
124
+ }));
125
+ return rows[0]?.id ?? 0;
126
+ }
package/dist/mcp-tools.js CHANGED
@@ -130,9 +130,9 @@ export async function collectMcpTools(opts = {}) {
130
130
  inputSchema: t.inputSchema,
131
131
  scope: 'plugin',
132
132
  role: t.role ?? 'member',
133
- handler: async (args) => {
133
+ handler: async (args, signal) => {
134
134
  try {
135
- const data = await t.handler(args, pluginCtx(id, getEm().fork()));
135
+ const data = await t.handler(args, pluginCtx(id, getEm().fork(), signal));
136
136
  return ok(data);
137
137
  }
138
138
  catch (e) {
@@ -162,7 +162,7 @@ export async function collectMcpTools(opts = {}) {
162
162
  },
163
163
  scope: 'rag',
164
164
  role: 'member',
165
- handler: async (args) => {
165
+ handler: async (args, signal) => {
166
166
  const library = args.library;
167
167
  const shelf = args.shelf;
168
168
  if (shelf && !library) {
@@ -180,7 +180,7 @@ export async function collectMcpTools(opts = {}) {
180
180
  }
181
181
  let vector = null;
182
182
  try {
183
- vector = (await embedOne(args.query, embeddingApiKey)).vector;
183
+ vector = (await embedOne(args.query, embeddingApiKey, undefined, signal)).vector;
184
184
  }
185
185
  catch (e) {
186
186
  log.warn(`embedding failed, text-only search — ${embeddingFailureMessage(e)}`);
@@ -0,0 +1,2 @@
1
+ export declare function shelfSources(slug: string, getShelfOwner: (library: string, shelf: string) => string | undefined): string[];
2
+ export declare function defaultShelfSources(slug: string): string[];
@@ -0,0 +1,22 @@
1
+ import { getActiveRegistry } from "../registry-context.js";
2
+ export function shelfSources(slug, getShelfOwner) {
3
+ const parts = slug.split('/');
4
+ if (parts.length !== 2)
5
+ return [];
6
+ const [library, shelf] = parts;
7
+ if (!library || !shelf)
8
+ return [];
9
+ const out = [`library:${library}`];
10
+ const owner = getShelfOwner(library, shelf);
11
+ if (owner)
12
+ out.push(`plugin:${owner}`);
13
+ return out;
14
+ }
15
+ export function defaultShelfSources(slug) {
16
+ try {
17
+ return shelfSources(slug, getActiveRegistry().getShelfOwner);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
@@ -3,6 +3,7 @@ import { getPluginState, setPluginState } from '../plugin-state.ts';
3
3
  import { collectStarterHints } from '../mcp-tools.ts';
4
4
  import { loadAgentId } from './config.ts';
5
5
  import { getDefaultAgentId } from './registry.ts';
6
+ import { listEventsSince, latestEventId } from '../embeddings.ts';
6
7
  import type { AgentRuntime } from './types.ts';
7
8
  export interface StartersDeps {
8
9
  getPluginSettings: typeof getPluginSettings;
@@ -14,6 +15,9 @@ export interface StartersDeps {
14
15
  getDefaultAgentId: typeof getDefaultAgentId;
15
16
  now: () => number;
16
17
  shuffle: <T>(xs: T[]) => T[];
18
+ listEventsSince: typeof listEventsSince;
19
+ latestEventId: typeof latestEventId;
20
+ shelfSources: (slug: string) => string[];
17
21
  }
18
22
  export declare function refreshSystemStarters(deps?: StartersDeps): Promise<void>;
19
23
  export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
@@ -5,14 +5,20 @@ import { collectStarterHints } from "../mcp-tools.js";
5
5
  import { getLogger } from '@coffer-org/sdk/logger';
6
6
  import { loadAgentId } from "./config.js";
7
7
  import { resolveAgent, getDefaultAgentId } from "./registry.js";
8
+ import { listEventsSince, latestEventId } from "../embeddings.js";
9
+ import { defaultShelfSources } from "./starter-sources.js";
8
10
  const log = getLogger('orchestrator');
9
11
  const PLUGIN = 'orchestrator';
10
12
  const STATE_KEY = 'system_starters';
11
13
  const POOL_VERSION = 2;
12
14
  const ENTRY_TTL_MS = 24 * 60 * 60_000;
15
+ const EARLY_DEBOUNCE_MS = 60 * 60_000;
16
+ const EARLY_COOLDOWN_MS = 6 * 60 * 60_000;
17
+ const EARLY_BUDGET_PER_DAY = 10;
13
18
  const SAMPLE_MIN = 4;
14
19
  const SAMPLE_MAX = 5;
15
20
  const PER_SOURCE_SOFT_CAP = 2;
21
+ const EVENT_BATCH = 5000;
16
22
  function shuffled(xs) {
17
23
  const out = [...xs];
18
24
  for (let i = out.length - 1; i > 0; i--) {
@@ -31,6 +37,9 @@ const defaultDeps = {
31
37
  getDefaultAgentId,
32
38
  now: () => Date.now(),
33
39
  shuffle: shuffled,
40
+ listEventsSince,
41
+ latestEventId,
42
+ shelfSources: defaultShelfSources,
34
43
  };
35
44
  async function currentLanguage(deps) {
36
45
  const system = await deps.getPluginSettings(SYSTEM_SETTINGS_ID);
@@ -58,6 +67,33 @@ async function readPool(deps) {
58
67
  function fresh(entry, nowMs) {
59
68
  return nowMs - Date.parse(entry.generatedAt) < ENTRY_TTL_MS;
60
69
  }
70
+ function dayKey(nowMs) {
71
+ return new Date(nowMs).toISOString().slice(0, 10);
72
+ }
73
+ function earlyBudgetLeft(pool, nowMs) {
74
+ const spent = pool.earlyRuns;
75
+ if (!spent || spent.date !== dayKey(nowMs))
76
+ return true;
77
+ return spent.count < EARLY_BUDGET_PER_DAY;
78
+ }
79
+ function countEarlyRun(pool, nowMs) {
80
+ const key = dayKey(nowMs);
81
+ pool.earlyRuns =
82
+ pool.earlyRuns?.date === key ? { date: key, count: pool.earlyRuns.count + 1 } : { date: key, count: 1 };
83
+ }
84
+ function pickEarly(pool, hints, nowMs) {
85
+ const at = (h) => Date.parse(pool.sources[h.id].generatedAt);
86
+ return hints
87
+ .filter((h) => {
88
+ const e = pool.sources[h.id];
89
+ if (!e?.dirtySince)
90
+ return false;
91
+ if (nowMs - Date.parse(e.dirtySince) < EARLY_DEBOUNCE_MS)
92
+ return false;
93
+ return nowMs - Date.parse(e.generatedAt) >= EARLY_COOLDOWN_MS;
94
+ })
95
+ .sort((a, b) => at(a) - at(b))[0];
96
+ }
61
97
  function tryResolve(deps, agentId) {
62
98
  try {
63
99
  return deps.resolveAgent(agentId);
@@ -66,6 +102,27 @@ function tryResolve(deps, agentId) {
66
102
  return undefined;
67
103
  }
68
104
  }
105
+ async function markChangedSources(deps, pool, declared, nowMs) {
106
+ if (pool.eventCursor == null) {
107
+ pool.eventCursor = await deps.latestEventId();
108
+ return true;
109
+ }
110
+ const rows = await deps.listEventsSince(pool.eventCursor, EVENT_BATCH);
111
+ if (!rows.length)
112
+ return false;
113
+ for (const row of rows) {
114
+ for (const id of deps.shelfSources(String(row.type ?? ''))) {
115
+ if (!declared.has(id))
116
+ continue;
117
+ const entry = pool.sources[id];
118
+ if (!entry || entry.dirtySince)
119
+ continue;
120
+ entry.dirtySince = new Date(nowMs).toISOString();
121
+ }
122
+ }
123
+ pool.eventCursor = rows[rows.length - 1].id;
124
+ return true;
125
+ }
69
126
  export async function refreshSystemStarters(deps = defaultDeps) {
70
127
  const [language, hints] = await Promise.all([currentLanguage(deps), deps.collectStarterHints()]);
71
128
  const agentId = (await deps.loadAgentId()) ?? deps.getDefaultAgentId();
@@ -82,6 +139,8 @@ export async function refreshSystemStarters(deps = defaultDeps) {
82
139
  }
83
140
  }
84
141
  const nowMs = deps.now();
142
+ if (await markChangedSources(deps, pool, declared, nowMs))
143
+ dirty = true;
85
144
  const stale = hints.find((h) => {
86
145
  const e = pool.sources[h.id];
87
146
  if (!e)
@@ -90,21 +149,25 @@ export async function refreshSystemStarters(deps = defaultDeps) {
90
149
  return true;
91
150
  return !fresh(e, nowMs);
92
151
  });
93
- if (stale) {
152
+ const early = stale || !earlyBudgetLeft(pool, nowMs) ? undefined : pickEarly(pool, hints, nowMs);
153
+ const target = stale ?? early;
154
+ if (target) {
94
155
  const runtime = tryResolve(deps, agentId);
95
156
  if (runtime?.starters) {
96
157
  try {
97
- const messages = await runtime.starters(stale.text);
98
- pool.sources[stale.id] = {
158
+ const messages = await runtime.starters(target.text);
159
+ pool.sources[target.id] = {
99
160
  messages,
100
161
  generatedAt: new Date(nowMs).toISOString(),
101
- hintHash: stale.hash,
162
+ hintHash: target.hash,
102
163
  agentId,
103
164
  };
165
+ if (target === early)
166
+ countEarlyRun(pool, nowMs);
104
167
  dirty = true;
105
168
  }
106
169
  catch (err) {
107
- log.warn(`starters for ${stale.id} failed: ${err instanceof Error ? err.message : String(err)}`);
170
+ log.warn(`starters for ${target.id} failed: ${err instanceof Error ? err.message : String(err)}`);
108
171
  }
109
172
  }
110
173
  }
@@ -25,8 +25,9 @@ export interface Migration {
25
25
  export interface PluginCtx {
26
26
  em: EntityManager;
27
27
  log: Logger;
28
+ signal?: AbortSignal;
28
29
  }
29
- export declare function pluginCtx(id: string, em: EntityManager): PluginCtx;
30
+ export declare function pluginCtx(id: string, em: EntityManager, signal?: AbortSignal): PluginCtx;
30
31
  export interface Seed {
31
32
  name: string;
32
33
  run(ctx: PluginCtx): Promise<void> | void;
@@ -1,6 +1,6 @@
1
1
  import { getLogger } from '@coffer-org/sdk/logger';
2
- export function pluginCtx(id, em) {
3
- return { em, log: getLogger(id) };
2
+ export function pluginCtx(id, em, signal) {
3
+ return { em, log: getLogger(id), ...(signal ? { signal } : {}) };
4
4
  }
5
5
  export class HttpError extends Error {
6
6
  status;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/server",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -36,7 +36,7 @@
36
36
  "postpack": "node ../../scripts/swap-exports.mjs src"
37
37
  },
38
38
  "dependencies": {
39
- "@coffer-org/sdk": "^7.0.0",
39
+ "@coffer-org/sdk": "^7.1.0",
40
40
  "@extractus/oembed-extractor": "^4.1.0",
41
41
  "@fastify/cors": "^11.2.0",
42
42
  "@fastify/multipart": "^10.0.0",