teapot-coding-agent 0.37.60 → 0.37.62
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 +40 -20
- package/dist/agent/llm.js +1 -1
- package/dist/master.js +0 -4
- package/dist/model-meta.js +176 -80
- package/dist/server/api.js +3 -1
- package/package.json +1 -1
- package/public/assets/{index-mEDytv26.css → index-Ce9MZA33.css} +1 -1
- package/public/assets/{index-DhJx7VDq.js → index-DUMcgXq7.js} +7 -7
- package/public/assets/index-DUMcgXq7.js.map +1 -0
- package/public/assets/{shiki-LBMkaWYb.js → shiki-Bech9FlU.js} +2 -2
- package/public/assets/{shiki-LBMkaWYb.js.map → shiki-Bech9FlU.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-DhJx7VDq.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -749,9 +749,39 @@ export class Agent {
|
|
|
749
749
|
});
|
|
750
750
|
return { ran };
|
|
751
751
|
}
|
|
752
|
-
|
|
752
|
+
/**
|
|
753
|
+
* Rewrite an earlier prompt and fork from there.
|
|
754
|
+
*
|
|
755
|
+
* The events after the edited prompt are DISCARDED, never summarized. A `summarize` mode used to
|
|
756
|
+
* call the model before forking, which made the edit block on a full round trip (the reported lag)
|
|
757
|
+
* and left `stopRequested` unset for the whole of it — so a second edit submitted while the first
|
|
758
|
+
* was summarizing read the same lineage and collided. Neither Pi nor OpenCode summarizes here, and
|
|
759
|
+
* the operator never used it, so the mode is gone rather than made asynchronous.
|
|
760
|
+
*/
|
|
761
|
+
async editPromptAt(eventId, text) {
|
|
753
762
|
if (this.status === "running" || this.parkedByTool || this.pendingPrompts.length)
|
|
754
763
|
throw new Error("agent is running — stop it before editing history");
|
|
764
|
+
/**
|
|
765
|
+
* A LATCH, because the status check above cannot see a second call.
|
|
766
|
+
*
|
|
767
|
+
* `readEvents` and the log writes are awaits, so two requests arriving together both passed the
|
|
768
|
+
* check, both read the same lineage, and both forked — MEASURED in tyb2-9-8155867c while a
|
|
769
|
+
* `summarize` was in flight, and reachable without it by double-submitting the dialog. The latch
|
|
770
|
+
* is released in `finally`, so a failed edit does not wedge the agent.
|
|
771
|
+
*/
|
|
772
|
+
if (this.editingHistory)
|
|
773
|
+
throw new Error("an edit is already in progress — wait for it to finish");
|
|
774
|
+
this.editingHistory = true;
|
|
775
|
+
try {
|
|
776
|
+
return await this.editPromptAtInner(eventId, text);
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
this.editingHistory = false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/** true while an `editPromptAt` call owns the log; see the latch in the public method */
|
|
783
|
+
editingHistory = false;
|
|
784
|
+
async editPromptAtInner(eventId, text) {
|
|
755
785
|
const all = await readEvents(this.log.filePath);
|
|
756
786
|
const target = all.find((e) => e.id === eventId);
|
|
757
787
|
if (!target || target.type !== "prompt")
|
|
@@ -763,23 +793,6 @@ export class Agent {
|
|
|
763
793
|
const kept = lineage.slice(0, tIdx);
|
|
764
794
|
const dropped = lineage.slice(tIdx);
|
|
765
795
|
const msgs = rebuildMessagesFrom(kept);
|
|
766
|
-
if (tail === "summarize" && dropped.length > 0) {
|
|
767
|
-
try {
|
|
768
|
-
const droppedMsgs = rebuildMessagesFrom(dropped);
|
|
769
|
-
if (droppedMsgs.length) {
|
|
770
|
-
const summary = await this.summarize(droppedMsgs);
|
|
771
|
-
if (summary.trim()) {
|
|
772
|
-
msgs.push({
|
|
773
|
-
role: "user",
|
|
774
|
-
content: "[harness] The conversation continued past this point on another timeline. " +
|
|
775
|
-
`Notes from what happened there:\n\n${summary}`,
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
796
|
msgs.push({ role: "user", content: text });
|
|
784
797
|
const newBranch = `br${this.branchCount()}${Date.now().toString(36).slice(-4)}`;
|
|
785
798
|
await this.quiesceForHistoryEdit();
|
|
@@ -790,7 +803,6 @@ export class Agent {
|
|
|
790
803
|
newBranch,
|
|
791
804
|
reason: "prompt-edited",
|
|
792
805
|
droppedEvents: dropped.length,
|
|
793
|
-
tailMode: tail,
|
|
794
806
|
});
|
|
795
807
|
const cutPoint = kept.at(-1)?.id ?? null;
|
|
796
808
|
this.log.seedBranch(newBranch, cutPoint);
|
|
@@ -1001,7 +1013,15 @@ export class Agent {
|
|
|
1001
1013
|
model: this.opts.llm.model,
|
|
1002
1014
|
provider: this.opts.provider,
|
|
1003
1015
|
...(this.opts.reasoningEffort ? { reasoningEffort: this.opts.reasoningEffort } : {}),
|
|
1004
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* Offer the control unless the catalog SAYS the model cannot reason.
|
|
1018
|
+
*
|
|
1019
|
+
* `effortChoosable(undefined)` is `true` on purpose: a model missing from models.dev is
|
|
1020
|
+
* UNKNOWN, not unsupported, and the wire recovers from a rejected `reasoning_effort`
|
|
1021
|
+
* (`isEffortUnsupported`). The previous rule gated this on OpenRouter alone, which hid the
|
|
1022
|
+
* control for every other provider whose model did support it.
|
|
1023
|
+
*/
|
|
1024
|
+
effortSupported: this.opts.supportedEfforts?.length ? true : effortChoosable(undefined),
|
|
1005
1025
|
...(this.opts.supportedEfforts?.length ? { effortOptions: this.opts.supportedEfforts } : {}),
|
|
1006
1026
|
sessionDir: this.opts.sessionDir,
|
|
1007
1027
|
ctx: {
|
package/dist/agent/llm.js
CHANGED
|
@@ -34,7 +34,7 @@ export function effortChoosable(supported) {
|
|
|
34
34
|
return true;
|
|
35
35
|
return supported.includes("reasoning_effort");
|
|
36
36
|
}
|
|
37
|
-
function isOpenRouterEndpoint(baseUrl) {
|
|
37
|
+
export function isOpenRouterEndpoint(baseUrl) {
|
|
38
38
|
try {
|
|
39
39
|
return /(^|\.)openrouter\.ai$/i.test(new URL(String(baseUrl ?? "")).host);
|
|
40
40
|
}
|
package/dist/master.js
CHANGED
|
@@ -659,9 +659,6 @@ export class Master {
|
|
|
659
659
|
...(ac.parent ? { parent: ac.parent } : {}),
|
|
660
660
|
...(ac.chatFn ? { chatFn: ac.chatFn } : {}),
|
|
661
661
|
...(ac.reasoningEffort ? { reasoningEffort: ac.reasoningEffort } : {}),
|
|
662
|
-
...(modelMeta?.supportedParameters
|
|
663
|
-
? { supportedParameters: modelMeta.supportedParameters }
|
|
664
|
-
: {}),
|
|
665
662
|
...(modelMeta?.supportedEfforts?.length ? { supportedEfforts: modelMeta.supportedEfforts } : {}),
|
|
666
663
|
});
|
|
667
664
|
void fetchModelList(llm.baseUrl, llm.apiKey)
|
|
@@ -1390,7 +1387,6 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1390
1387
|
if (cfgEntry)
|
|
1391
1388
|
cfgEntry.reasoningEffort = opts.opts.reasoningEffort || undefined;
|
|
1392
1389
|
}
|
|
1393
|
-
opts.opts.supportedParameters = meta?.supportedParameters;
|
|
1394
1390
|
opts.opts.supportedEfforts = meta?.supportedEfforts;
|
|
1395
1391
|
if (!llm.model)
|
|
1396
1392
|
throw new Error("no model resolved (pass model or set one on the provider)");
|
package/dist/model-meta.js
CHANGED
|
@@ -1,84 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model capabilities, from models.dev.
|
|
3
|
+
*
|
|
4
|
+
* WHY models.dev and not the provider's own `/models`: MEASURED against the operator's four
|
|
5
|
+
* configured providers, only OpenRouter publishes anything about capabilities. Command Code (93
|
|
6
|
+
* models), Google (38) and orcarouter all return just an id, a name and a context length — no
|
|
7
|
+
* `modalities`, no `reasoning`, no `supported_parameters`. That is why this file used to conclude
|
|
8
|
+
* "cannot be known", which was WRONG: models.dev carries the same information for 232 providers,
|
|
9
|
+
* including `commandcode` (71 models) and `opencode` (130), keyed by the provider's own API host.
|
|
10
|
+
*
|
|
11
|
+
* The shape mirrors what Pi (and its forks) do — models.dev is the authority, `reasoning_options`
|
|
12
|
+
* becomes the selectable effort list, `modalities.input` becomes the accepted input kinds. Keeping a
|
|
13
|
+
* second parser for each provider's ad-hoc `/models` JSON was the "wasteful and a source of bugs"
|
|
14
|
+
* the operator called out, so it is gone.
|
|
15
|
+
*/
|
|
1
16
|
import { providerHeaders } from "./agent/provider-headers.js";
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
const TTL =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const DB_URL = "https://models.dev/api.json";
|
|
18
|
+
/** the whole DB is ~6MB; it changes on the order of days, so an hour is generous and cheap. */
|
|
19
|
+
const TTL = 60 * 60_000;
|
|
20
|
+
/** a failed fetch must not re-run per agent (15s × every agent at boot was the old bug) */
|
|
21
|
+
const NEGATIVE_TTL = 5 * 60_000;
|
|
22
|
+
let db;
|
|
23
|
+
let inFlight;
|
|
24
|
+
const hostOf = (baseUrl) => {
|
|
25
|
+
try {
|
|
26
|
+
return new URL(String(baseUrl ?? "")).host.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const fresh = (s) => !!s && Date.now() - s.at < (s.negative ? NEGATIVE_TTL : TTL);
|
|
33
|
+
/**
|
|
34
|
+
* Find the models.dev provider for a baseUrl.
|
|
35
|
+
*
|
|
36
|
+
* HOST FIRST, then the path as a tie-break, because both are needed:
|
|
37
|
+
*
|
|
38
|
+
* - a host match is what makes `commandcode` work: FIVE config entries share
|
|
39
|
+
* `api.commandcode.ai`, and models.dev lists that host once;
|
|
40
|
+
* - a host match alone is ambiguous for `opencode.ai`, which models.dev splits into `opencode`
|
|
41
|
+
* (130 models, `/inference/openai/v1`) and `opencode-go` (37, `/inference/go/openai/v1`).
|
|
42
|
+
* MEASURED: host-only picked whichever came first and resolved the session's provider to the
|
|
43
|
+
* wrong catalog — 37 models, and its model not found.
|
|
44
|
+
*
|
|
45
|
+
* The operator's `https://opencode.ai/zen/v1` matches NEITHER path exactly, so the path only
|
|
46
|
+
* disambiguates when it actually agrees; otherwise the host match stands.
|
|
47
|
+
*/
|
|
48
|
+
export function providerFor(dbProviders, baseUrl) {
|
|
49
|
+
const host = hostOf(baseUrl);
|
|
50
|
+
if (!host)
|
|
15
51
|
return undefined;
|
|
16
|
-
|
|
52
|
+
const sameHost = [...dbProviders.values()].filter((p) => hostOf(p.api ?? undefined) === host);
|
|
53
|
+
if (sameHost.length <= 1)
|
|
54
|
+
return sameHost[0];
|
|
55
|
+
const path = (() => {
|
|
56
|
+
try {
|
|
57
|
+
return new URL(String(baseUrl ?? "")).pathname;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
// prefer the entry whose path is a prefix/suffix relation with ours, longest overlap wins
|
|
64
|
+
const scored = sameHost
|
|
65
|
+
.map((p) => {
|
|
66
|
+
const apiPath = (() => {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(String(p.api ?? "")).pathname;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
let common = 0;
|
|
75
|
+
for (let i = 0; i < Math.min(apiPath.length, path.length) && apiPath[i] === path[i]; i++)
|
|
76
|
+
common++;
|
|
77
|
+
/**
|
|
78
|
+
* When the shared prefix is only the leading `/`, the paths disagree entirely — `zen/v1` is
|
|
79
|
+
* neither `inference/openai/v1` nor `inference/go/openai/v1`. In that case prefer the entry
|
|
80
|
+
* with MORE models: a real provider catalog is the general one, and the narrower variant is a
|
|
81
|
+
* strict subset (opencode: 130 vs opencode-go: 37, MEASURED).
|
|
82
|
+
*/
|
|
83
|
+
const models = Object.keys(p.models ?? {}).length;
|
|
84
|
+
return { p, common, models };
|
|
85
|
+
})
|
|
86
|
+
.sort((a, b) => b.common - a.common || b.models - a.models);
|
|
87
|
+
return scored[0]?.p ?? sameHost[0];
|
|
17
88
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
23
|
-
const inflight = inFlight.get(root);
|
|
24
|
-
if (inflight)
|
|
25
|
-
return inflight;
|
|
89
|
+
async function loadDb() {
|
|
90
|
+
if (fresh(db))
|
|
91
|
+
return db;
|
|
92
|
+
if (inFlight)
|
|
93
|
+
return inFlight;
|
|
26
94
|
const p = (async () => {
|
|
27
95
|
try {
|
|
28
|
-
const res = await fetch(
|
|
29
|
-
headers:
|
|
30
|
-
|
|
31
|
-
...providerHeaders(baseUrl),
|
|
32
|
-
},
|
|
33
|
-
signal: AbortSignal.timeout(3_000),
|
|
96
|
+
const res = await fetch(DB_URL, {
|
|
97
|
+
headers: providerHeaders(DB_URL),
|
|
98
|
+
signal: AbortSignal.timeout(8_000),
|
|
34
99
|
});
|
|
35
100
|
if (!res.ok)
|
|
36
101
|
throw new Error(`HTTP ${res.status}`);
|
|
37
102
|
const j = (await res.json());
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
: undefined,
|
|
48
|
-
modalities: Array.isArray(m.architecture?.input_modalities) && m.architecture.input_modalities.length
|
|
49
|
-
? {
|
|
50
|
-
input: m.architecture.input_modalities,
|
|
51
|
-
output: m.architecture.output_modalities ?? ["text"],
|
|
52
|
-
}
|
|
53
|
-
: undefined,
|
|
54
|
-
supportedParameters: Array.isArray(m.supported_parameters)
|
|
55
|
-
? m.supported_parameters.filter((p) => typeof p === "string")
|
|
56
|
-
: undefined,
|
|
57
|
-
supportedEfforts: Array.isArray(m.reasoning?.supported_efforts)
|
|
58
|
-
? m.reasoning.supported_efforts.filter((e) => typeof e === "string")
|
|
59
|
-
: undefined,
|
|
60
|
-
...(typeof m.reasoning?.default_effort === "string"
|
|
61
|
-
? { defaultEffort: m.reasoning.default_effort }
|
|
62
|
-
: {}),
|
|
63
|
-
...(m.reasoning?.mandatory === true ? { reasoningMandatory: true } : {}),
|
|
64
|
-
}))
|
|
65
|
-
.filter((m) => m.id);
|
|
66
|
-
cache.set(root, { at: Date.now(), list });
|
|
67
|
-
return list;
|
|
103
|
+
const providers = new Map();
|
|
104
|
+
for (const [key, value] of Object.entries(j)) {
|
|
105
|
+
if (value && typeof value === "object")
|
|
106
|
+
providers.set(key, value);
|
|
107
|
+
}
|
|
108
|
+
const snap = { at: Date.now(), providers };
|
|
109
|
+
db = snap;
|
|
110
|
+
return snap;
|
|
68
111
|
}
|
|
69
112
|
catch {
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Remember the FAILURE briefly, and always return a usable snapshot.
|
|
115
|
+
*
|
|
116
|
+
* Without the negative cache every agent paid an 8s timeout at boot — the same shape as the
|
|
117
|
+
* old per-endpoint fetch. An empty map is not an error to the caller: it means "no data", and
|
|
118
|
+
* every consumer treats absent metadata as UNKNOWN rather than as "unsupported".
|
|
119
|
+
*/
|
|
120
|
+
const snap = { at: Date.now(), providers: new Map(), negative: true };
|
|
121
|
+
db = snap;
|
|
122
|
+
return snap;
|
|
72
123
|
}
|
|
73
124
|
})();
|
|
74
|
-
inFlight
|
|
125
|
+
inFlight = p;
|
|
75
126
|
try {
|
|
76
127
|
return await p;
|
|
77
128
|
}
|
|
78
129
|
finally {
|
|
79
|
-
inFlight
|
|
130
|
+
inFlight = undefined;
|
|
80
131
|
}
|
|
81
132
|
}
|
|
133
|
+
/** the effort levels a models.dev record publishes, flattened out of `reasoning_options` */
|
|
134
|
+
export function effortsFrom(m) {
|
|
135
|
+
const values = (m?.reasoning_options ?? [])
|
|
136
|
+
.filter((o) => o?.type === "effort")
|
|
137
|
+
.flatMap((o) => (Array.isArray(o.values) ? o.values : []))
|
|
138
|
+
.filter((v) => typeof v === "string");
|
|
139
|
+
return values.length ? [...new Set(values)] : undefined;
|
|
140
|
+
}
|
|
141
|
+
const strings = (v) => {
|
|
142
|
+
const out = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string");
|
|
143
|
+
return out.length ? out : undefined;
|
|
144
|
+
};
|
|
145
|
+
/** one models.dev record → our shape; unknown fields stay absent, never invented */
|
|
146
|
+
export function metaFrom(m) {
|
|
147
|
+
const input = strings(m.modalities?.input);
|
|
148
|
+
const output = strings(m.modalities?.output);
|
|
149
|
+
const efforts = effortsFrom(m);
|
|
150
|
+
return {
|
|
151
|
+
id: String(m.id ?? ""),
|
|
152
|
+
...(typeof m.name === "string" ? { name: m.name } : {}),
|
|
153
|
+
...(typeof m.limit?.context === "number" ? { contextLength: m.limit.context } : {}),
|
|
154
|
+
...(m.cost && (typeof m.cost.input === "number" || typeof m.cost.output === "number")
|
|
155
|
+
? { pricing: { prompt: Number(m.cost.input ?? 0), completion: Number(m.cost.output ?? 0) } }
|
|
156
|
+
: {}),
|
|
157
|
+
...(input ? { modalities: { input, output: output ?? ["text"] } } : {}),
|
|
158
|
+
...(efforts ? { supportedEfforts: efforts } : {}),
|
|
159
|
+
...(m.reasoning === true ? { reasoning: true } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** every model models.dev lists for this baseUrl's provider, or [] when the provider is unknown */
|
|
163
|
+
export async function fetchModelList(baseUrl, _apiKey) {
|
|
164
|
+
const snap = await loadDb();
|
|
165
|
+
const provider = providerFor(snap.providers, baseUrl);
|
|
166
|
+
if (!provider?.models)
|
|
167
|
+
return [];
|
|
168
|
+
return Object.values(provider.models).map(metaFrom).filter((m) => m.id);
|
|
169
|
+
}
|
|
170
|
+
export function peekModelList(baseUrl) {
|
|
171
|
+
if (!fresh(db))
|
|
172
|
+
return undefined;
|
|
173
|
+
const provider = providerFor(db.providers, baseUrl);
|
|
174
|
+
if (!provider?.models)
|
|
175
|
+
return undefined;
|
|
176
|
+
return Object.values(provider.models).map(metaFrom).filter((m) => m.id);
|
|
177
|
+
}
|
|
82
178
|
export function contextLengthFor(list, model) {
|
|
83
179
|
return list.find((m) => m.id === model)?.contextLength;
|
|
84
180
|
}
|
|
@@ -86,23 +182,23 @@ export const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
|
86
182
|
export function isReasoningEffort(v) {
|
|
87
183
|
return typeof v === "string" && REASONING_EFFORTS.includes(v);
|
|
88
184
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!isOpenRouter(opts.baseUrl))
|
|
101
|
-
return undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Should the UI offer a reasoning-effort control for this model?
|
|
187
|
+
*
|
|
188
|
+
* THREE answers, not two. `true` when models.dev says the model reasons; `false` when it says it
|
|
189
|
+
* does not; and TRUE when the model is simply not in the database — because the previous rule
|
|
190
|
+
* ("only OpenRouter") hid the control for every other provider even though the request would have
|
|
191
|
+
* worked. A wire that rejects `reasoning_effort` is recovered from at runtime
|
|
192
|
+
* (`isEffortUnsupported`), so an optimistic default costs one retry at worst, while a pessimistic one
|
|
193
|
+
* silently removes a feature the model does have.
|
|
194
|
+
*/
|
|
195
|
+
export function effortSupportedFor(opts) {
|
|
102
196
|
const meta = opts.list?.find((m) => m.id === opts.model);
|
|
103
|
-
if (!meta
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
197
|
+
if (!meta)
|
|
198
|
+
return true; // not in models.dev → unknown, not "unsupported"
|
|
199
|
+
return meta.reasoning === true || !!meta.supportedEfforts?.length;
|
|
200
|
+
}
|
|
201
|
+
/** the effort levels to offer, or undefined to let the caller use its default ladder */
|
|
202
|
+
export function effortsFor(opts) {
|
|
203
|
+
return opts.list?.find((m) => m.id === opts.model)?.supportedEfforts;
|
|
108
204
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -1152,7 +1152,9 @@ export function buildApp(master) {
|
|
|
1152
1152
|
if (!body?.eventId || !body.text?.trim())
|
|
1153
1153
|
return c.json({ error: "eventId and text required" }, 400);
|
|
1154
1154
|
try {
|
|
1155
|
-
|
|
1155
|
+
// a `tail` in the body is ignored on purpose: the events after the edit are discarded, so an
|
|
1156
|
+
// older client still sending `tail: "summarize"` gets the fast path rather than an error
|
|
1157
|
+
const r = await a.editPromptAt(body.eventId, body.text);
|
|
1156
1158
|
a.start("edited prompt resent");
|
|
1157
1159
|
return c.json({ ok: true, ...r });
|
|
1158
1160
|
}
|