teapot-coding-agent 0.29.5 → 0.29.7
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/agent/agent.js +19 -3
- package/dist/master.js +140 -17
- package/dist/model-meta.js +18 -0
- package/package.json +1 -1
- package/public/assets/{index-DIjwa8pX.js → index-DPfeY2n-.js} +4 -4
- package/public/assets/{index-DIjwa8pX.js.map → index-DPfeY2n-.js.map} +1 -1
- package/public/assets/{shiki-C_RJDjY3.js → shiki-BQcHFGv4.js} +2 -2
- package/public/assets/{shiki-C_RJDjY3.js.map → shiki-BQcHFGv4.js.map} +1 -1
- package/public/index.html +1 -1
package/dist/agent/agent.js
CHANGED
|
@@ -628,9 +628,25 @@ export class Agent {
|
|
|
628
628
|
this.goal = stored ?? { text: "", status: "active", updatedAt: new Date().toISOString() };
|
|
629
629
|
// operator-maintained task list lives beside goal.md
|
|
630
630
|
this.todo = await fs.readFile(this.todoFile, "utf8").catch(() => "");
|
|
631
|
-
|
|
632
|
-
//
|
|
633
|
-
//
|
|
631
|
+
// #143: NO skill scan here. It was the single largest boot cost — 493 agents
|
|
632
|
+
// each walking their roots (readdir + stat, recursively) measured 2120 ms of
|
|
633
|
+
// a 4.7 s boot. Every one of those walks was redundant:
|
|
634
|
+
//
|
|
635
|
+
// - the turn loop calls `refreshSkills()` at the TOP of every turn, before
|
|
636
|
+
// the system prompt (which embeds the catalogue) is built, so the scan is
|
|
637
|
+
// always current before any LLM request
|
|
638
|
+
// - the `list_skills` tool calls it itself before reading the cache
|
|
639
|
+
// - `/api/agents/:id/skills` (what the panel uses) does its own fresh
|
|
640
|
+
// `currentSkills()` walk and never reads this cache
|
|
641
|
+
//
|
|
642
|
+
// Caching the walk was the other option and was WORSE: the fingerprint exists
|
|
643
|
+
// to notice a NESTED skill's add/edit, which does not change its root's mtime,
|
|
644
|
+
// so any TTL/mtime key reintroduces the staleness the walk was written to fix
|
|
645
|
+
// (the skills-fingerprint tests pin exactly that). Skipping boot's copy costs
|
|
646
|
+
// no freshness at all.
|
|
647
|
+
//
|
|
648
|
+
// The conversation is likewise not restored here: boot cost stays O(agents),
|
|
649
|
+
// not O(history). Both are rebuilt lazily by ensureReady()/the turn loop.
|
|
634
650
|
if (this.opts.restoreSession) {
|
|
635
651
|
this.status = "stopped";
|
|
636
652
|
this.statusReason = "session not loaded — select it or send a prompt";
|
package/dist/master.js
CHANGED
|
@@ -13,9 +13,53 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import { Agent } from "./agent/agent.js";
|
|
14
14
|
import { parseSchedule, matches, nextFireAt } from "./scheduler/cron.js";
|
|
15
15
|
import { bus } from "./bus.js";
|
|
16
|
-
import { fetchModelList, contextLengthFor, isReasoningEffort, } from "./model-meta.js";
|
|
16
|
+
import { fetchModelList, peekModelList, contextLengthFor, isReasoningEffort, } from "./model-meta.js";
|
|
17
17
|
/** how deep sub-agent spawning may nest (parent=0, its subs=1, …) */
|
|
18
|
+
/**
|
|
19
|
+
* Did the effective data dir come from an ENV OVERRIDE that disagrees with the
|
|
20
|
+
* config file's own `dataDir`? (#143)
|
|
21
|
+
*
|
|
22
|
+
* When it does, the config's session pointers are meaningless: they name
|
|
23
|
+
* directories under the OLD root, and this process is looking at a different
|
|
24
|
+
* one. Every agent therefore mints a fresh session — and `recordSession` writes
|
|
25
|
+
* those new pointers back into the config, destroying the real mapping. The
|
|
26
|
+
* caller then throws the new root away (a test's temp dir), leaving a config that
|
|
27
|
+
* points at sessions that never existed.
|
|
28
|
+
*
|
|
29
|
+
* That is not hypothetical: `test/cli-boot-and-host.test.ts` spawns the built CLI
|
|
30
|
+
* with `TEAPOT_DATA_DIR=<temp>` but NO config override, so `pnpm test` on a
|
|
31
|
+
* developer machine rewrote their real config and orphaned 493 sessions' pointers
|
|
32
|
+
* — twice, before this guard. On CI it is invisible, because CI has no config
|
|
33
|
+
* file to damage.
|
|
34
|
+
*
|
|
35
|
+
* So: a mismatched root makes session resolution READ-ONLY. The boot still works
|
|
36
|
+
* (agents get working sessions in the new root) — it just does not rewrite a
|
|
37
|
+
* config that is describing somebody else's data dir.
|
|
38
|
+
*/
|
|
39
|
+
function dataDirOverrideMismatch(configFilePath, effective) {
|
|
40
|
+
if (!process.env.TEAPOT_DATA_DIR)
|
|
41
|
+
return false; // not an override: the file is the truth
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(readFileSync(configFilePath, "utf8"));
|
|
44
|
+
if (typeof raw.dataDir !== "string" || !raw.dataDir)
|
|
45
|
+
return false;
|
|
46
|
+
return path.resolve(raw.dataDir) !== path.resolve(effective);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false; // unreadable/new config — nothing to protect
|
|
50
|
+
}
|
|
51
|
+
}
|
|
18
52
|
const MAX_SPAWN_DEPTH = 3;
|
|
53
|
+
/**
|
|
54
|
+
* How long boot waits for an endpoint's model catalogue (#143).
|
|
55
|
+
*
|
|
56
|
+
* The window and the reasoning-effort gate both come from `/models`, so a boot
|
|
57
|
+
* that waits on it is waiting for metadata, not for anything it needs to start.
|
|
58
|
+
* The lookup is fired concurrently for every distinct endpoint before the agent
|
|
59
|
+
* loop (see prefetchModelLists), so this bound is paid once per ENDPOINT rather
|
|
60
|
+
* than once per agent — and only when the endpoint is slow or dead.
|
|
61
|
+
*/
|
|
62
|
+
const MODEL_META_BOOT_MS = 1_500;
|
|
19
63
|
/** statuses that mean a child has finished working, for #130 */
|
|
20
64
|
const SETTLED = new Set(["idle", "stopped", "waiting", "error"]);
|
|
21
65
|
/**
|
|
@@ -312,6 +356,16 @@ export class Master {
|
|
|
312
356
|
constructor(config, configPath) {
|
|
313
357
|
this.config = config;
|
|
314
358
|
this.configPath = configPath;
|
|
359
|
+
// #143: an env data-dir override that disagrees with the config file means
|
|
360
|
+
// this config describes a DIFFERENT data dir — its session pointers cannot be
|
|
361
|
+
// valid here, so rewriting them would destroy the real mapping. See
|
|
362
|
+
// dataDirOverrideMismatch().
|
|
363
|
+
this.sessionsReadOnly = dataDirOverrideMismatch(configPath, config.dataDir);
|
|
364
|
+
if (this.sessionsReadOnly) {
|
|
365
|
+
console.warn(`[teapot] TEAPOT_DATA_DIR (${config.dataDir}) disagrees with the config's own dataDir; ` +
|
|
366
|
+
`sessions will be resolved in the override but NOT written back to ${configPath}. ` +
|
|
367
|
+
`Set TEAPOT_CONFIG to an isolated config to silence this.`);
|
|
368
|
+
}
|
|
315
369
|
}
|
|
316
370
|
/**
|
|
317
371
|
* Persist current logical config back to disk (lossless via raw user config).
|
|
@@ -362,6 +416,21 @@ export class Master {
|
|
|
362
416
|
*/
|
|
363
417
|
deferConfigWrites = false;
|
|
364
418
|
configDirty = false;
|
|
419
|
+
/**
|
|
420
|
+
* Endpoints whose catalogue boot has already WAITED for (#143).
|
|
421
|
+
*
|
|
422
|
+
* The wait must be paid once per endpoint, not once per agent. `Promise.race`
|
|
423
|
+
* against a timeout is per-CALL: with a dead endpoint every agent on it paid
|
|
424
|
+
* the full timeout again, because the shared request was still pending. 493
|
|
425
|
+
* agents over one unreachable provider measured 4.4s of boot spent purely
|
|
426
|
+
* idle, for a single request.
|
|
427
|
+
*/
|
|
428
|
+
metaWaited = new Set();
|
|
429
|
+
/**
|
|
430
|
+
* #143: `TEAPOT_DATA_DIR` points somewhere other than the config's own dataDir,
|
|
431
|
+
* so session pointers must not be written back (see dataDirOverrideMismatch).
|
|
432
|
+
*/
|
|
433
|
+
sessionsReadOnly = false;
|
|
365
434
|
/** write once and clear the dirty flag — used at the end of a batched section */
|
|
366
435
|
flushConfig() {
|
|
367
436
|
this.configDirty = false;
|
|
@@ -495,6 +564,14 @@ export class Master {
|
|
|
495
564
|
// boot, which is its own silent data loss.
|
|
496
565
|
this.deferConfigWrites = true;
|
|
497
566
|
this.configDirty = false;
|
|
567
|
+
// #143: start every endpoint's model-catalogue lookup NOW, concurrently.
|
|
568
|
+
//
|
|
569
|
+
// `addAgent` awaits this per agent, and the endpoints are shared by many
|
|
570
|
+
// agents (493 agents over 6 providers here). Fired up front, the requests
|
|
571
|
+
// overlap and each subsequent agent finds the promise already in flight or
|
|
572
|
+
// resolved — instead of the requests being serialised behind the agent loop,
|
|
573
|
+
// where a slow endpoint's timeout is paid again and again.
|
|
574
|
+
this.prefetchModelLists();
|
|
498
575
|
try {
|
|
499
576
|
for (const ac of this.config.agents) {
|
|
500
577
|
try {
|
|
@@ -596,6 +673,38 @@ export class Master {
|
|
|
596
673
|
* Restarts reuse the latest existing session; fresh creations never touch
|
|
597
674
|
* an older one's history.
|
|
598
675
|
*/
|
|
676
|
+
/**
|
|
677
|
+
* Fire the model-catalogue lookup for every distinct endpoint, concurrently.
|
|
678
|
+
*
|
|
679
|
+
* `fetchModelList` already shares an in-flight request per baseUrl, so this is
|
|
680
|
+
* about STARTING them together: without it the requests begin one at a time, as
|
|
681
|
+
* the agent loop reaches each provider, and a slow endpoint's timeout is paid
|
|
682
|
+
* serially by every agent on it.
|
|
683
|
+
*
|
|
684
|
+
* Best-effort by design — nothing awaits this. A failure is cached briefly by
|
|
685
|
+
* `fetchModelList`, and an agent whose lookup is not ready simply gets no
|
|
686
|
+
* inferred window, which is the same outcome as today's timeout.
|
|
687
|
+
*/
|
|
688
|
+
prefetchModelLists() {
|
|
689
|
+
const endpoints = new Set();
|
|
690
|
+
const add = (baseUrl, apiKey) => {
|
|
691
|
+
if (!baseUrl)
|
|
692
|
+
return;
|
|
693
|
+
const key = baseUrl.replace(/\/+$/, "");
|
|
694
|
+
if (endpoints.has(key))
|
|
695
|
+
return;
|
|
696
|
+
endpoints.add(key);
|
|
697
|
+
void fetchModelList(baseUrl, apiKey).catch(() => []);
|
|
698
|
+
};
|
|
699
|
+
for (const ac of this.config.agents) {
|
|
700
|
+
const prov = this.config.providers?.[ac.provider ?? this.config.defaultProvider ?? ""];
|
|
701
|
+
add(ac.baseUrl ?? prov?.baseUrl ?? this.config.llm.baseUrl, ac.apiKey ?? prov?.apiKey ?? this.config.llm.apiKey);
|
|
702
|
+
}
|
|
703
|
+
// no configured agents yet (first-run wizard) — still warm the default so the
|
|
704
|
+
// first created agent does not wait
|
|
705
|
+
if (endpoints.size === 0)
|
|
706
|
+
add(this.config.llm.baseUrl, this.config.llm.apiKey);
|
|
707
|
+
}
|
|
599
708
|
async addAgent(ac, opts = {}) {
|
|
600
709
|
// #72: an agent id becomes a DIRECTORY name (sessions/<id>-<hash>), so it
|
|
601
710
|
// must be a safe path segment. Every caller sanitised its own id — the HTTP
|
|
@@ -631,24 +740,33 @@ export class Master {
|
|
|
631
740
|
// spawn-tree depth: computed from the config chain (ac may not be
|
|
632
741
|
// registered yet when this runs — spawnChildFor persists AFTER creation)
|
|
633
742
|
const myDepth = ac.parent ? this.depthOf(ac.parent) + 1 : 0;
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
743
|
+
// Model metadata: the context window (for the compaction budget) and the
|
|
744
|
+
// catalogue entry that gates reasoning_effort support (#47) come from the
|
|
745
|
+
// SAME `/models` list.
|
|
746
|
+
//
|
|
747
|
+
// #143: boot must not WAIT for this. It only tunes the compaction budget and
|
|
748
|
+
// the effort controls, so a missing list costs nothing that matters; what it
|
|
749
|
+
// cost was boot time — measured at 4.4s of pure idle on 493 agents pointed at
|
|
750
|
+
// one unreachable endpoint, because the `Promise.race` timeout below is
|
|
751
|
+
// per-CALL and the shared request stayed pending for every agent.
|
|
752
|
+
//
|
|
753
|
+
// So: use the catalogue if `prefetchModelLists()` has already produced it
|
|
754
|
+
// (the common case — the lookups were fired when boot started), and wait only
|
|
755
|
+
// ONCE per endpoint for the stragglers, where the wait overlaps a request
|
|
756
|
+
// that is already in flight.
|
|
757
|
+
const needsWindow = !ac.contextWindowTokens && !this.config.contextWindowTokens;
|
|
758
|
+
const root = llm.baseUrl.replace(/\/+$/, "");
|
|
759
|
+
let list = peekModelList(llm.baseUrl);
|
|
760
|
+
if (list === undefined && !this.metaWaited.has(root)) {
|
|
761
|
+
this.metaWaited.add(root);
|
|
762
|
+
list = await Promise.race([
|
|
763
|
+
fetchModelList(llm.baseUrl, llm.apiKey),
|
|
764
|
+
// a placeholder so the race can settle without the list
|
|
765
|
+
new Promise((r) => setTimeout(() => r(undefined), MODEL_META_BOOT_MS)),
|
|
644
766
|
]);
|
|
645
|
-
void listPromise.catch(() => []);
|
|
646
767
|
}
|
|
647
|
-
|
|
648
|
-
const modelMeta = (
|
|
649
|
-
fetchModelList(llm.baseUrl, llm.apiKey),
|
|
650
|
-
new Promise((r) => setTimeout(() => r(undefined), 1_500)),
|
|
651
|
-
]))?.find((m) => m.id === llm.model);
|
|
768
|
+
const inferredWindow = needsWindow && list ? contextLengthFor(list, llm.model) : undefined;
|
|
769
|
+
const modelMeta = list?.find((m) => m.id === llm.model);
|
|
652
770
|
const sessionDir = this.resolveSessionDir(ac.id, opts.fresh === true);
|
|
653
771
|
await mkdirSync(sessionDir, { recursive: true });
|
|
654
772
|
// every session dir this master hands out carries an ownership manifest —
|
|
@@ -806,6 +924,11 @@ export class Master {
|
|
|
806
924
|
if (!entry)
|
|
807
925
|
return; // not a configured agent — nothing to persist yet
|
|
808
926
|
entry.session = sessionId;
|
|
927
|
+
// #143: never write session pointers into a config that is describing another
|
|
928
|
+
// data dir — every one of them would be wrong, and the correct mapping is
|
|
929
|
+
// destroyed. The in-memory session above still works for this process.
|
|
930
|
+
if (this.sessionsReadOnly)
|
|
931
|
+
return;
|
|
809
932
|
this.saveConfig();
|
|
810
933
|
}
|
|
811
934
|
/** spawn-tree depth of an agent (0 = top level); unknown → 0 */
|
package/dist/model-meta.js
CHANGED
|
@@ -26,6 +26,24 @@ const NEGATIVE_TTL = 30_000;
|
|
|
26
26
|
function fresh(entry) {
|
|
27
27
|
return Date.now() - entry.at < (entry.negative ? NEGATIVE_TTL : TTL);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The catalogue if it is ALREADY known, without starting a fetch (#143).
|
|
31
|
+
*
|
|
32
|
+
* Boot uses this so it never waits for metadata it can do without: the context
|
|
33
|
+
* window only tunes the compaction budget, and `supportedParameters` only gates
|
|
34
|
+
* the reasoning-effort controls (#47). `prefetchModelLists()` starts the real
|
|
35
|
+
* lookups in parallel, so an endpoint that answers quickly is usually warm by the
|
|
36
|
+
* time its first agent is created, and a slow or dead one costs no boot time.
|
|
37
|
+
*/
|
|
38
|
+
export function peekModelList(baseUrl) {
|
|
39
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
40
|
+
const hit = cache.get(root);
|
|
41
|
+
if (!hit)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!fresh(hit))
|
|
44
|
+
return undefined;
|
|
45
|
+
return hit.negative ? [] : hit.list;
|
|
46
|
+
}
|
|
29
47
|
/** Fetch GET /models from an OpenAI-compatible endpoint (cached ~10 min). */
|
|
30
48
|
export async function fetchModelList(baseUrl, apiKey) {
|
|
31
49
|
const root = baseUrl.replace(/\/+$/, "");
|