negotium 0.1.37 → 0.1.39
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/README.md +19 -0
- package/dist/agent-helpers.js +15637 -5674
- package/dist/agent-helpers.js.map +85 -37
- package/dist/browser-runtime.js +1600 -0
- package/dist/browser-runtime.js.map +28 -0
- package/dist/hosted-agent.js +8 -8
- package/dist/hosted-agent.js.map +6 -5
- package/dist/main.js +3790 -3113
- package/dist/main.js.map +33 -29
- package/dist/mcp-factories.js +15141 -301
- package/dist/mcp-factories.js.map +100 -7
- package/dist/prompts.js +117 -49
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +310 -1
- package/dist/query-runtime.js.map +10 -5
- package/dist/runtime/src/agents/archiver.ts +407 -296
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +440 -274
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +139 -65
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +1037 -816
- package/dist/runtime/src/agents/public-helpers.ts +82 -0
- package/dist/runtime/src/agents/self-config-core.ts +574 -278
- package/dist/runtime/src/agents/topic-cleanup.ts +167 -129
- package/dist/runtime/src/index.ts +4 -1
- package/dist/runtime/src/mcp/factories/index.ts +9 -0
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +132 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +23 -57
- package/dist/runtime/src/platform/mcp-config.ts +2 -4
- package/dist/runtime/src/platform/playwright/browser-processes.ts +3 -2
- package/dist/runtime/src/platform/playwright/capability.ts +5 -0
- package/dist/runtime/src/platform/playwright/manager.ts +380 -66
- package/dist/runtime/src/platform/playwright/public-runtime.ts +45 -0
- package/dist/runtime/src/platform/playwright/transport-probe.ts +89 -0
- package/dist/runtime/src/prompts/builders.ts +192 -51
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/query/public-runtime.ts +8 -0
- package/dist/runtime/src/query/state.ts +69 -33
- package/dist/runtime/src/query/types.ts +1 -0
- package/dist/runtime/src/runtime/playwright-turn-abort.ts +59 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +23 -6
- package/dist/runtime/src/runtime/turn-runner.ts +6 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +38 -0
- package/dist/types/packages/core/src/agents/archiver.d.ts +52 -16
- package/dist/types/packages/core/src/agents/mcp-tools/ask-user.d.ts +75 -4
- package/dist/types/packages/core/src/agents/mcp-tools/self-config.d.ts +17 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +112 -25
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +7 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +185 -0
- package/dist/types/packages/core/src/agents/topic-cleanup.d.ts +30 -24
- package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +46 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +2 -1
- package/dist/types/packages/core/src/platform/playwright/capability.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +65 -5
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +4 -0
- package/dist/types/packages/core/src/platform/playwright/transport-probe.d.ts +13 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +29 -3
- package/dist/types/packages/core/src/query/active-rooms.d.ts +2 -0
- package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
- package/dist/types/packages/core/src/query/state.d.ts +13 -0
- package/dist/types/packages/core/src/query/types.d.ts +2 -1
- package/dist/types/packages/core/src/runtime/playwright-turn-abort.d.ts +12 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +6 -1
|
@@ -12,20 +12,15 @@ import {
|
|
|
12
12
|
getPendingSelfSchedule,
|
|
13
13
|
updatePendingSelfSchedule,
|
|
14
14
|
} from "#storage/self-schedules";
|
|
15
|
-
import {
|
|
16
|
-
createDerivedTopic,
|
|
17
|
-
TopicForkCompactionError,
|
|
18
|
-
TopicTitleConflictError,
|
|
19
|
-
} from "#topics/derive";
|
|
20
15
|
import { topicMarkdownLink } from "#topics/links";
|
|
21
16
|
import type { AgentKind, EffortLevel } from "#types";
|
|
22
|
-
import type { TopicDto } from "#types/api";
|
|
23
17
|
|
|
24
18
|
export const SELF_CONFIG_MCP_KEY = "topic-config";
|
|
25
|
-
|
|
26
|
-
export type SelfConfigField = "agent" | "model" | "effort";
|
|
27
19
|
export const SELF_SCHEDULE_MAX_DELAY_SECONDS = 86_400;
|
|
28
20
|
export const SELF_SCHEDULE_MAX_MESSAGE_LENGTH = 10_000;
|
|
21
|
+
export const SELF_CONFIG_DERIVED_TOPIC_LIMIT = 5;
|
|
22
|
+
|
|
23
|
+
export type SelfConfigField = "agent" | "model" | "effort";
|
|
29
24
|
|
|
30
25
|
export interface SelfConfigContext {
|
|
31
26
|
topicId: string;
|
|
@@ -42,42 +37,186 @@ export interface SelfConfigResult {
|
|
|
42
37
|
isError?: boolean;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
export interface SelfConfigTopic {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
agent?: AgentKind;
|
|
44
|
+
defaultModel: string;
|
|
45
|
+
defaultEffort: EffortLevel;
|
|
46
|
+
participants: readonly { userId: string }[];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
export interface SelfConfigTopicConfig {
|
|
50
|
+
model?: string;
|
|
51
|
+
effort?: EffortLevel;
|
|
52
|
+
mcp?: string[];
|
|
53
|
+
agentLocked?: boolean;
|
|
54
|
+
modelLocked?: boolean;
|
|
55
|
+
effortLocked?: boolean;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
export interface SelfConfigAgentPolicy {
|
|
59
|
+
defaultEffort?: EffortLevel;
|
|
60
|
+
validEfforts: readonly EffortLevel[];
|
|
61
|
+
validateModel(model: string): boolean;
|
|
62
|
+
validateEffort(effort: EffortLevel): boolean;
|
|
63
|
+
resolveModel(model: string | undefined): string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SelfConfigTopicStore {
|
|
67
|
+
getTopic(topicId: string): SelfConfigTopic | null;
|
|
68
|
+
getConfig(topicId: string): SelfConfigTopicConfig | undefined;
|
|
69
|
+
setConfig(topicId: string, config: SelfConfigTopicConfig): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SelfConfigModelPolicy {
|
|
73
|
+
forAgent(agent: AgentKind): SelfConfigAgentPolicy;
|
|
74
|
+
owner(model: string): AgentKind | undefined;
|
|
75
|
+
checkAuth(
|
|
76
|
+
agent: AgentKind,
|
|
77
|
+
model: string,
|
|
78
|
+
userId: string,
|
|
79
|
+
): { ok: true } | { ok: false; error: string };
|
|
61
80
|
}
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return topic;
|
|
82
|
+
export interface SelfConfigRuntimeBoundary {
|
|
83
|
+
resolveWorkspaceDir(topicId: string): string;
|
|
84
|
+
switchAgent(options: SelfConfigAgentSwitchOptions): SelfConfigAgentSwitchResult;
|
|
85
|
+
configChanged(topicId: string, field: SelfConfigField): void;
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
export type SelfConfigAgentSwitchResult =
|
|
89
|
+
| {
|
|
90
|
+
ok: true;
|
|
91
|
+
outcome:
|
|
92
|
+
| { kind: "fresh"; agent: AgentKind; reason: "no-history" | "bridge-failed" }
|
|
93
|
+
| {
|
|
94
|
+
kind: "bridged";
|
|
95
|
+
agent: AgentKind;
|
|
96
|
+
bridgedSessionId: string;
|
|
97
|
+
rolloutPath: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
| { ok: false; error: string };
|
|
101
|
+
|
|
102
|
+
export interface SelfConfigAgentSwitchOptions {
|
|
103
|
+
topicId: string;
|
|
104
|
+
topicTitle: string;
|
|
105
|
+
userId: string;
|
|
106
|
+
fromAgent?: AgentKind;
|
|
107
|
+
agent: AgentKind;
|
|
108
|
+
cwd: string;
|
|
109
|
+
config: SelfConfigTopicConfig;
|
|
110
|
+
defaultModel?: string;
|
|
111
|
+
defaultEffort?: EffortLevel;
|
|
112
|
+
reason: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SelfConfigDerivedTopics {
|
|
116
|
+
create(
|
|
117
|
+
sourceTopicId: string,
|
|
118
|
+
userId: string,
|
|
119
|
+
copyHistory: boolean,
|
|
120
|
+
options: { name?: string },
|
|
121
|
+
): Promise<SelfConfigTopic | null>;
|
|
122
|
+
link(topicId: string): string;
|
|
123
|
+
isTitleConflict(error: unknown): boolean;
|
|
124
|
+
isForkCompactionError(error: unknown): boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SelfConfigSchedules {
|
|
128
|
+
create(input: {
|
|
129
|
+
topicId: string;
|
|
130
|
+
userId: string;
|
|
131
|
+
message: string;
|
|
132
|
+
deliverAt: number;
|
|
133
|
+
now?: number;
|
|
134
|
+
}): SelfConfigCreateScheduleResult;
|
|
135
|
+
getPending(topicId: string): SelfConfigSchedule | null;
|
|
136
|
+
update(input: {
|
|
137
|
+
topicId: string;
|
|
138
|
+
scheduleId: string;
|
|
139
|
+
message?: string;
|
|
140
|
+
deliverAt?: number;
|
|
141
|
+
now?: number;
|
|
142
|
+
}): SelfConfigSchedule | null;
|
|
143
|
+
cancel(topicId: string, scheduleId: string): boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SelfConfigSchedule {
|
|
147
|
+
id: string;
|
|
148
|
+
topicId: string;
|
|
149
|
+
userId: string;
|
|
150
|
+
message: string;
|
|
151
|
+
deliverAt: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type SelfConfigCreateScheduleResult =
|
|
155
|
+
| { ok: true; schedule: SelfConfigSchedule }
|
|
156
|
+
| { ok: false; existing: SelfConfigSchedule };
|
|
157
|
+
|
|
158
|
+
/** Product-level limits and copy that are shared by policy and MCP wrappers. */
|
|
159
|
+
export interface SelfConfigProductConfig {
|
|
160
|
+
readonly mcpKey: string;
|
|
161
|
+
readonly scheduleMaxDelaySeconds: number;
|
|
162
|
+
readonly scheduleMaxMessageLength: number;
|
|
163
|
+
readonly derivedTopicLimit: number;
|
|
164
|
+
readonly toolDescriptions: Readonly<Record<string, string>>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface SelfConfigHost {
|
|
168
|
+
topics: SelfConfigTopicStore;
|
|
169
|
+
models: SelfConfigModelPolicy;
|
|
170
|
+
runtime: SelfConfigRuntimeBoundary;
|
|
171
|
+
derivedTopics?: SelfConfigDerivedTopics;
|
|
172
|
+
schedules?: SelfConfigSchedules;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SelfConfigCore {
|
|
176
|
+
readonly host: SelfConfigHost;
|
|
177
|
+
readonly product: SelfConfigProductConfig;
|
|
178
|
+
setModel(ctx: SelfConfigContext, model: string): SelfConfigResult;
|
|
179
|
+
getModel(ctx: SelfConfigContext): SelfConfigResult;
|
|
180
|
+
setAgent(ctx: SelfConfigContext, agent: AgentKind): SelfConfigResult;
|
|
181
|
+
getAgent(ctx: SelfConfigContext): SelfConfigResult;
|
|
182
|
+
setEffort(ctx: SelfConfigContext, effort: EffortLevel): SelfConfigResult;
|
|
183
|
+
getEffort(ctx: SelfConfigContext): SelfConfigResult;
|
|
184
|
+
scheduleContinue(
|
|
185
|
+
ctx: SelfConfigContext,
|
|
186
|
+
delaySeconds: number,
|
|
187
|
+
message: string,
|
|
188
|
+
nowMs?: number,
|
|
189
|
+
): SelfConfigResult;
|
|
190
|
+
getSchedule(ctx: SelfConfigContext, nowMs?: number): SelfConfigResult;
|
|
191
|
+
updateSchedule(
|
|
192
|
+
ctx: SelfConfigContext,
|
|
193
|
+
scheduleId: string,
|
|
194
|
+
updates: { delaySeconds?: number; message?: string },
|
|
195
|
+
nowMs?: number,
|
|
196
|
+
): SelfConfigResult;
|
|
197
|
+
cancelSchedule(ctx: SelfConfigContext, scheduleId: string): SelfConfigResult;
|
|
198
|
+
spawnTopic(ctx: SelfConfigContext, name?: string): Promise<SelfConfigResult>;
|
|
199
|
+
forkTopic(ctx: SelfConfigContext, name?: string): Promise<SelfConfigResult>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const DEFAULT_SELF_CONFIG_PRODUCT: SelfConfigProductConfig = {
|
|
203
|
+
mcpKey: SELF_CONFIG_MCP_KEY,
|
|
204
|
+
scheduleMaxDelaySeconds: SELF_SCHEDULE_MAX_DELAY_SECONDS,
|
|
205
|
+
scheduleMaxMessageLength: SELF_SCHEDULE_MAX_MESSAGE_LENGTH,
|
|
206
|
+
derivedTopicLimit: SELF_CONFIG_DERIVED_TOPIC_LIMIT,
|
|
207
|
+
toolDescriptions: {},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function ok(text: string): SelfConfigResult {
|
|
211
|
+
return { text };
|
|
72
212
|
}
|
|
73
213
|
|
|
74
|
-
function
|
|
75
|
-
return
|
|
214
|
+
function err(text: string): SelfConfigResult {
|
|
215
|
+
return { text, isError: true };
|
|
76
216
|
}
|
|
77
217
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
return resolveModelForAgent(agent, topic.defaultModel, registry);
|
|
218
|
+
function errorMessage(error: unknown): string {
|
|
219
|
+
return error instanceof Error ? error.message : String(error);
|
|
81
220
|
}
|
|
82
221
|
|
|
83
222
|
function escapeRegExp(value: string): string {
|
|
@@ -111,8 +250,7 @@ function agentAliases(agent: AgentKind): string[] {
|
|
|
111
250
|
function hasExplicitAgentSwitchRequest(prompt: string | undefined, agent: AgentKind): boolean {
|
|
112
251
|
if (!prompt?.trim()) return false;
|
|
113
252
|
const text = prompt.toLowerCase().replace(/\s+/g, " ").trim();
|
|
114
|
-
const
|
|
115
|
-
const target = `(?:${aliases.join("|")})`;
|
|
253
|
+
const target = `(?:${agentAliases(agent).map(escapeRegExp).join("|")})`;
|
|
116
254
|
const switchVerb =
|
|
117
255
|
"(?:바꿔|바꿔줘|변경|변경해|전환|전환해|설정|설정해|써줘|사용|가|switch|change|set|use)";
|
|
118
256
|
const switchSubject = "(?:agent|runtime|model|에이전트|런타임|모델)";
|
|
@@ -126,205 +264,435 @@ function hasExplicitAgentSwitchRequest(prompt: string | undefined, agent: AgentK
|
|
|
126
264
|
].some((pattern) => pattern.test(text));
|
|
127
265
|
}
|
|
128
266
|
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
267
|
+
export function createSelfConfigCore(
|
|
268
|
+
host: SelfConfigHost,
|
|
269
|
+
productOverrides: Partial<SelfConfigProductConfig> = {},
|
|
270
|
+
): SelfConfigCore {
|
|
271
|
+
const product: SelfConfigProductConfig = Object.freeze({
|
|
272
|
+
...DEFAULT_SELF_CONFIG_PRODUCT,
|
|
273
|
+
...productOverrides,
|
|
274
|
+
toolDescriptions: Object.freeze({
|
|
275
|
+
...DEFAULT_SELF_CONFIG_PRODUCT.toolDescriptions,
|
|
276
|
+
...productOverrides.toolDescriptions,
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
for (const [name, value] of [
|
|
280
|
+
["scheduleMaxDelaySeconds", product.scheduleMaxDelaySeconds],
|
|
281
|
+
["scheduleMaxMessageLength", product.scheduleMaxMessageLength],
|
|
282
|
+
["derivedTopicLimit", product.derivedTopicLimit],
|
|
283
|
+
] as const) {
|
|
284
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
285
|
+
throw new Error(`self-config product ${name} must be a positive integer`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function requireAccessibleTopic(ctx: SelfConfigContext): SelfConfigTopic | SelfConfigResult {
|
|
290
|
+
if (!ctx.topicId || !ctx.userId) return err("Error: missing topicId/userId context.");
|
|
291
|
+
const topic = host.topics.getTopic(ctx.topicId);
|
|
292
|
+
if (!topic) return err(`Error: topic '${ctx.topicId}' not found.`);
|
|
293
|
+
if (!topic.participants.some((participant) => participant.userId === ctx.userId)) {
|
|
294
|
+
return err("Error: user is not a member of this topic.");
|
|
295
|
+
}
|
|
296
|
+
return topic;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function requireTopic(ctx: SelfConfigContext): SelfConfigTopic | SelfConfigResult {
|
|
300
|
+
const topic = requireAccessibleTopic(ctx);
|
|
301
|
+
if ("text" in topic) return topic;
|
|
302
|
+
if (!topic.agent) return err("Error: this topic has no AI agent invited.");
|
|
303
|
+
return topic;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function currentAgent(topic: SelfConfigTopic): AgentKind {
|
|
307
|
+
return topic.agent ?? "maestro";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function setModel(ctx: SelfConfigContext, model: string): SelfConfigResult {
|
|
311
|
+
const topic = requireTopic(ctx);
|
|
312
|
+
if ("text" in topic) return topic;
|
|
313
|
+
|
|
314
|
+
const config = host.topics.getConfig(topic.id) ?? {};
|
|
315
|
+
if (config.modelLocked) {
|
|
316
|
+
return err("Model for this topic is locked by the user. Cannot override.");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const agent = currentAgent(topic);
|
|
320
|
+
const policy = host.models.forAgent(agent);
|
|
321
|
+
const owner = host.models.owner(model);
|
|
322
|
+
if ((owner && owner !== agent) || !policy.validateModel(model)) {
|
|
323
|
+
return err(
|
|
324
|
+
`'${model}' is not a valid model for agent '${agent}'. If it belongs to another agent, call set_agent first.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const resolvedModel = policy.resolveModel(model);
|
|
328
|
+
const auth = host.models.checkAuth(agent, resolvedModel, ctx.userId);
|
|
329
|
+
if (!auth.ok) return err(auth.error);
|
|
330
|
+
|
|
331
|
+
host.topics.setConfig(topic.id, { ...config, model: resolvedModel });
|
|
332
|
+
host.runtime.configChanged(topic.id, "model");
|
|
333
|
+
ctx.onConfigChanged?.("model");
|
|
334
|
+
return ok(
|
|
335
|
+
`Model for this topic set to '${resolvedModel}' (agent=${agent}). Applies from the next turn.`,
|
|
142
336
|
);
|
|
143
337
|
}
|
|
144
|
-
const resolvedModel = resolveModelForAgent(agent, model, registry);
|
|
145
|
-
const auth = checkAgentModelAuth(agent, resolvedModel, undefined, ctx.userId);
|
|
146
|
-
if (!auth.ok) return err(auth.error);
|
|
147
338
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
339
|
+
function getModel(ctx: SelfConfigContext): SelfConfigResult {
|
|
340
|
+
const topic = requireTopic(ctx);
|
|
341
|
+
if ("text" in topic) return topic;
|
|
342
|
+
|
|
343
|
+
const config = host.topics.getConfig(topic.id);
|
|
344
|
+
const agent = currentAgent(topic);
|
|
345
|
+
const policy = host.models.forAgent(agent);
|
|
346
|
+
const fallback = policy.resolveModel(topic.defaultModel);
|
|
347
|
+
const resolved = config?.model ? policy.resolveModel(config.model) : fallback;
|
|
348
|
+
const value =
|
|
349
|
+
config?.model && resolved === config.model ? config.model : `default (${fallback})`;
|
|
350
|
+
const lock = config?.modelLocked ? " [locked by user]" : "";
|
|
351
|
+
return ok(`Model (agent=${agent}): ${value}${lock}`);
|
|
352
|
+
}
|
|
157
353
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
354
|
+
function setAgent(ctx: SelfConfigContext, agent: AgentKind): SelfConfigResult {
|
|
355
|
+
const topic = requireTopic(ctx);
|
|
356
|
+
if ("text" in topic) return topic;
|
|
161
357
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
358
|
+
const config = host.topics.getConfig(topic.id) ?? {};
|
|
359
|
+
const existing = currentAgent(topic);
|
|
360
|
+
if (existing === agent) return ok(`Agent is already '${agent}'. No change.`);
|
|
361
|
+
if (config.agentLocked) {
|
|
362
|
+
return err("Agent for this topic is locked by the user. Cannot override.");
|
|
363
|
+
}
|
|
364
|
+
if (!hasExplicitAgentSwitchRequest(ctx.currentUserPrompt, agent)) {
|
|
365
|
+
return err(
|
|
366
|
+
`Agent switch to '${agent}' requires an explicit request in the current user message.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
171
369
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
370
|
+
const switched = host.runtime.switchAgent({
|
|
371
|
+
topicId: topic.id,
|
|
372
|
+
topicTitle: topic.title,
|
|
373
|
+
userId: ctx.userId,
|
|
374
|
+
fromAgent: existing,
|
|
375
|
+
agent,
|
|
376
|
+
cwd: ctx.cwd ?? host.runtime.resolveWorkspaceDir(topic.id),
|
|
377
|
+
config: { ...config, model: undefined, effort: undefined },
|
|
378
|
+
defaultModel: topic.defaultModel,
|
|
379
|
+
defaultEffort: topic.defaultEffort,
|
|
380
|
+
reason: "self-config-agent-switch",
|
|
381
|
+
});
|
|
382
|
+
if (!switched.ok) return err(switched.error);
|
|
383
|
+
|
|
384
|
+
host.runtime.configChanged(topic.id, "agent");
|
|
385
|
+
ctx.onConfigChanged?.("agent");
|
|
386
|
+
const sessionNote =
|
|
387
|
+
switched.outcome.kind === "bridged"
|
|
388
|
+
? ` Conversation history was bridged into a ${agent} session (${switched.outcome.bridgedSessionId}).`
|
|
389
|
+
: switched.outcome.reason === "bridge-failed"
|
|
390
|
+
? " Conversation history bridge failed; the next turn starts fresh."
|
|
391
|
+
: " No prior conversation history was found; the next turn starts fresh.";
|
|
392
|
+
return ok(
|
|
393
|
+
`Agent for this topic set to '${agent}'. Model/effort reset to '${agent}' defaults.${sessionNote}`,
|
|
185
394
|
);
|
|
186
395
|
}
|
|
187
396
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
topicId: topic.id,
|
|
194
|
-
topicTitle: topic.title,
|
|
195
|
-
userId: ctx.userId,
|
|
196
|
-
fromAgent: existing,
|
|
197
|
-
agent,
|
|
198
|
-
cwd: ctx.cwd ?? resolveTopicWorkspaceDir(topic.id),
|
|
199
|
-
config: { ...cfg, model: undefined, effort: undefined },
|
|
200
|
-
defaultModel: topic.defaultModel,
|
|
201
|
-
defaultEffort: topic.defaultEffort,
|
|
202
|
-
reason: "self-config-agent-switch",
|
|
203
|
-
});
|
|
204
|
-
if (!switched.ok) return err(switched.error);
|
|
397
|
+
function getAgent(ctx: SelfConfigContext): SelfConfigResult {
|
|
398
|
+
const topic = requireTopic(ctx);
|
|
399
|
+
if ("text" in topic) return topic;
|
|
400
|
+
return ok(`Agent: ${currentAgent(topic)}`);
|
|
401
|
+
}
|
|
205
402
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
switched.outcome.kind === "bridged"
|
|
210
|
-
? ` Conversation history was bridged into a ${agent} session (${switched.outcome.bridgedSessionId}).`
|
|
211
|
-
: switched.outcome.reason === "bridge-failed"
|
|
212
|
-
? " Conversation history bridge failed; the next turn starts fresh."
|
|
213
|
-
: " No prior conversation history was found; the next turn starts fresh.";
|
|
214
|
-
return ok(
|
|
215
|
-
`Agent for this topic set to '${agent}'. Model/effort reset to '${agent}' defaults.${sessionNote}`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
403
|
+
function setEffort(ctx: SelfConfigContext, effort: EffortLevel): SelfConfigResult {
|
|
404
|
+
const topic = requireTopic(ctx);
|
|
405
|
+
if ("text" in topic) return topic;
|
|
218
406
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
407
|
+
const config = host.topics.getConfig(topic.id) ?? {};
|
|
408
|
+
if (config.effortLocked) {
|
|
409
|
+
return err("Effort for this topic is locked by the user. Cannot override.");
|
|
410
|
+
}
|
|
411
|
+
const agent = currentAgent(topic);
|
|
412
|
+
const policy = host.models.forAgent(agent);
|
|
413
|
+
if (!policy.validateEffort(effort)) {
|
|
414
|
+
return err(
|
|
415
|
+
`'${effort}' is not a valid effort for agent '${agent}'. Valid: ${policy.validEfforts.join(", ")}.`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
222
418
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
419
|
+
host.topics.setConfig(topic.id, { ...config, effort });
|
|
420
|
+
host.runtime.configChanged(topic.id, "effort");
|
|
421
|
+
ctx.onConfigChanged?.("effort");
|
|
422
|
+
return ok(
|
|
423
|
+
`Effort for this topic set to '${effort}' (agent=${agent}). Applies from the next turn.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
226
426
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
427
|
+
function getEffort(ctx: SelfConfigContext): SelfConfigResult {
|
|
428
|
+
const topic = requireTopic(ctx);
|
|
429
|
+
if ("text" in topic) return topic;
|
|
430
|
+
|
|
431
|
+
const config = host.topics.getConfig(topic.id);
|
|
432
|
+
const agent = currentAgent(topic);
|
|
433
|
+
const policy = host.models.forAgent(agent);
|
|
434
|
+
const fallback = policy.defaultEffort ? `default (${policy.defaultEffort})` : "default (off)";
|
|
435
|
+
const value = config?.effort ?? fallback;
|
|
436
|
+
const lock = config?.effortLocked ? " [locked by user]" : "";
|
|
437
|
+
return ok(`Effort (agent=${agent}): ${value}${lock}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function unavailable(capability: string): SelfConfigResult {
|
|
441
|
+
return err(`${capability} is not available in this host.`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function scheduleContinue(
|
|
445
|
+
ctx: SelfConfigContext,
|
|
446
|
+
delaySeconds: number,
|
|
447
|
+
message: string,
|
|
448
|
+
nowMs = Date.now(),
|
|
449
|
+
): SelfConfigResult {
|
|
450
|
+
if (!host.schedules) return unavailable("Self-scheduling");
|
|
451
|
+
const topic = requireTopic(ctx);
|
|
452
|
+
if ("text" in topic) return topic;
|
|
453
|
+
if (
|
|
454
|
+
!Number.isInteger(delaySeconds) ||
|
|
455
|
+
delaySeconds < 1 ||
|
|
456
|
+
delaySeconds > product.scheduleMaxDelaySeconds
|
|
457
|
+
) {
|
|
458
|
+
return err(`delay_seconds must be an integer from 1 to ${product.scheduleMaxDelaySeconds}.`);
|
|
459
|
+
}
|
|
460
|
+
const cleanMessage = message.trim();
|
|
461
|
+
if (!cleanMessage) return err("message is required.");
|
|
462
|
+
if (cleanMessage.length > product.scheduleMaxMessageLength) {
|
|
463
|
+
return err(`message must be ${product.scheduleMaxMessageLength} characters or fewer.`);
|
|
464
|
+
}
|
|
230
465
|
|
|
231
|
-
|
|
232
|
-
|
|
466
|
+
const deliverAtMs = nowMs + delaySeconds * 1000;
|
|
467
|
+
const created = host.schedules.create({
|
|
468
|
+
topicId: topic.id,
|
|
469
|
+
userId: ctx.userId,
|
|
470
|
+
message: cleanMessage,
|
|
471
|
+
deliverAt: deliverAtMs,
|
|
472
|
+
now: nowMs,
|
|
473
|
+
});
|
|
474
|
+
if (!created.ok) {
|
|
475
|
+
return err(
|
|
476
|
+
`This topic already has a pending self-schedule (${created.existing.id}) for ` +
|
|
477
|
+
`${new Date(created.existing.deliverAt).toISOString()}. ` +
|
|
478
|
+
"Use update_self_schedule or cancel_self_schedule instead of creating another one.",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return ok(
|
|
482
|
+
`Scheduled this topic to resume in ${delaySeconds} seconds at ${new Date(deliverAtMs).toISOString()}. ` +
|
|
483
|
+
`Schedule ID: ${created.schedule.id}. ` +
|
|
484
|
+
`The continuation is durable across node restarts (delivery granularity is about 5 seconds).`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
233
487
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return
|
|
238
|
-
|
|
488
|
+
function getSchedule(ctx: SelfConfigContext, nowMs = Date.now()): SelfConfigResult {
|
|
489
|
+
if (!host.schedules) return unavailable("Self-scheduling");
|
|
490
|
+
const topic = requireAccessibleTopic(ctx);
|
|
491
|
+
if ("text" in topic) return topic;
|
|
492
|
+
const schedule = host.schedules.getPending(topic.id);
|
|
493
|
+
if (!schedule) return ok("This topic has no pending self-schedule.");
|
|
494
|
+
const remainingSeconds = Math.max(0, Math.ceil((schedule.deliverAt - nowMs) / 1000));
|
|
495
|
+
return ok(
|
|
496
|
+
[
|
|
497
|
+
`Pending self-schedule: ${schedule.id}`,
|
|
498
|
+
`Deliver at: ${new Date(schedule.deliverAt).toISOString()} (in about ${remainingSeconds} seconds)`,
|
|
499
|
+
`Message: ${schedule.message}`,
|
|
500
|
+
].join("\n"),
|
|
239
501
|
);
|
|
240
502
|
}
|
|
241
503
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
504
|
+
function updateSchedule(
|
|
505
|
+
ctx: SelfConfigContext,
|
|
506
|
+
scheduleId: string,
|
|
507
|
+
updates: { delaySeconds?: number; message?: string },
|
|
508
|
+
nowMs = Date.now(),
|
|
509
|
+
): SelfConfigResult {
|
|
510
|
+
if (!host.schedules) return unavailable("Self-scheduling");
|
|
511
|
+
const topic = requireAccessibleTopic(ctx);
|
|
512
|
+
if ("text" in topic) return topic;
|
|
513
|
+
if (updates.delaySeconds === undefined && updates.message === undefined) {
|
|
514
|
+
return err("Provide delay_seconds, message, or both.");
|
|
515
|
+
}
|
|
516
|
+
if (
|
|
517
|
+
updates.delaySeconds !== undefined &&
|
|
518
|
+
(!Number.isInteger(updates.delaySeconds) ||
|
|
519
|
+
updates.delaySeconds < 1 ||
|
|
520
|
+
updates.delaySeconds > product.scheduleMaxDelaySeconds)
|
|
521
|
+
) {
|
|
522
|
+
return err(`delay_seconds must be an integer from 1 to ${product.scheduleMaxDelaySeconds}.`);
|
|
523
|
+
}
|
|
524
|
+
let cleanMessage: string | undefined;
|
|
525
|
+
if (updates.message !== undefined) {
|
|
526
|
+
cleanMessage = updates.message.trim();
|
|
527
|
+
if (!cleanMessage) return err("message is required when provided.");
|
|
528
|
+
if (cleanMessage.length > product.scheduleMaxMessageLength) {
|
|
529
|
+
return err(`message must be ${product.scheduleMaxMessageLength} characters or fewer.`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const updated = host.schedules.update({
|
|
534
|
+
topicId: topic.id,
|
|
535
|
+
scheduleId,
|
|
536
|
+
message: cleanMessage,
|
|
537
|
+
deliverAt:
|
|
538
|
+
updates.delaySeconds === undefined ? undefined : nowMs + updates.delaySeconds * 1000,
|
|
539
|
+
now: nowMs,
|
|
540
|
+
});
|
|
541
|
+
if (!updated) {
|
|
542
|
+
return err(
|
|
543
|
+
`Pending self-schedule '${scheduleId}' was not found. It may have been cancelled, replaced, or already started.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return ok(
|
|
547
|
+
`Updated self-schedule ${updated.id}. It will resume at ${new Date(updated.deliverAt).toISOString()}.`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function cancelSchedule(ctx: SelfConfigContext, scheduleId: string): SelfConfigResult {
|
|
552
|
+
if (!host.schedules) return unavailable("Self-scheduling");
|
|
553
|
+
const topic = requireAccessibleTopic(ctx);
|
|
554
|
+
if ("text" in topic) return topic;
|
|
555
|
+
if (!host.schedules.cancel(topic.id, scheduleId)) {
|
|
556
|
+
return err(
|
|
557
|
+
`Pending self-schedule '${scheduleId}' was not found. It may have been cancelled, replaced, or already started.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return ok(`Cancelled self-schedule ${scheduleId}.`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function deriveTopic(
|
|
564
|
+
ctx: SelfConfigContext,
|
|
565
|
+
copyHistory: boolean,
|
|
566
|
+
name?: string,
|
|
567
|
+
): Promise<SelfConfigResult> {
|
|
568
|
+
if (!host.derivedTopics) return unavailable("Derived topics");
|
|
569
|
+
const topic = requireTopic(ctx);
|
|
570
|
+
if ("text" in topic) return topic;
|
|
571
|
+
|
|
572
|
+
let derived: SelfConfigTopic | null;
|
|
573
|
+
try {
|
|
574
|
+
derived = await host.derivedTopics.create(topic.id, ctx.userId, copyHistory, { name });
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (host.derivedTopics.isTitleConflict(error)) {
|
|
577
|
+
return err(`${errorMessage(error)} — pick a different name.`);
|
|
578
|
+
}
|
|
579
|
+
if (copyHistory && host.derivedTopics.isForkCompactionError(error)) {
|
|
580
|
+
return err(errorMessage(error));
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
const verb = copyHistory ? "Forked" : "Spawned";
|
|
585
|
+
if (!derived) {
|
|
586
|
+
return err(`Failed to ${verb.toLowerCase()} topic (source not found or permission denied).`);
|
|
587
|
+
}
|
|
588
|
+
const history = copyHistory ? " History copied." : "";
|
|
589
|
+
return ok(
|
|
590
|
+
`${verb} new topic "${derived.title}".${history}\nLink: ${host.derivedTopics.link(derived.id)}`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const core: SelfConfigCore = {
|
|
595
|
+
host,
|
|
596
|
+
product,
|
|
597
|
+
setModel,
|
|
598
|
+
getModel,
|
|
599
|
+
setAgent,
|
|
600
|
+
getAgent,
|
|
601
|
+
setEffort,
|
|
602
|
+
getEffort,
|
|
603
|
+
scheduleContinue,
|
|
604
|
+
getSchedule,
|
|
605
|
+
updateSchedule,
|
|
606
|
+
cancelSchedule,
|
|
607
|
+
spawnTopic: (ctx, name) => deriveTopic(ctx, false, name),
|
|
608
|
+
forkTopic: (ctx, name) => deriveTopic(ctx, true, name),
|
|
609
|
+
};
|
|
610
|
+
return Object.freeze(core);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export const defaultSelfConfigHost: SelfConfigHost = {
|
|
614
|
+
topics: {
|
|
615
|
+
getTopic,
|
|
616
|
+
getConfig: getApiTopicConfig,
|
|
617
|
+
setConfig: setApiTopicConfig,
|
|
618
|
+
},
|
|
619
|
+
models: {
|
|
620
|
+
forAgent(agent) {
|
|
621
|
+
const registry = getRegistry(agent);
|
|
622
|
+
return {
|
|
623
|
+
defaultEffort: registry.defaultEffort,
|
|
624
|
+
validEfforts: registry.validEfforts,
|
|
625
|
+
validateModel: (model) => registry.validateModel(model),
|
|
626
|
+
validateEffort: (effort) => registry.validateEffort(effort),
|
|
627
|
+
resolveModel: (model) => resolveModelForAgent(agent, model, registry),
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
owner: modelOwner,
|
|
631
|
+
checkAuth: (agent, model, userId) => checkAgentModelAuth(agent, model, undefined, userId),
|
|
632
|
+
},
|
|
633
|
+
runtime: {
|
|
634
|
+
resolveWorkspaceDir: resolveTopicWorkspaceDir,
|
|
635
|
+
switchAgent: switchApiTopicAgent,
|
|
636
|
+
configChanged: (topicId) => WsHub.get().broadcastTopicUpdated(topicId),
|
|
637
|
+
},
|
|
638
|
+
derivedTopics: {
|
|
639
|
+
async create(sourceTopicId, userId, copyHistory, options) {
|
|
640
|
+
const { createDerivedTopic } = await import("#topics/derive");
|
|
641
|
+
return createDerivedTopic(sourceTopicId, userId, copyHistory, options);
|
|
642
|
+
},
|
|
643
|
+
link: topicMarkdownLink,
|
|
644
|
+
isTitleConflict: (error) => error instanceof Error && error.name === "TopicTitleConflictError",
|
|
645
|
+
isForkCompactionError: (error) =>
|
|
646
|
+
error instanceof Error && error.name === "TopicForkCompactionError",
|
|
647
|
+
},
|
|
648
|
+
schedules: {
|
|
649
|
+
create: createPendingSelfSchedule,
|
|
650
|
+
getPending: getPendingSelfSchedule,
|
|
651
|
+
update: updatePendingSelfSchedule,
|
|
652
|
+
cancel: cancelPendingSelfSchedule,
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
export const defaultSelfConfigCore = createSelfConfigCore(defaultSelfConfigHost);
|
|
657
|
+
|
|
658
|
+
export function setSelfConfigModel(ctx: SelfConfigContext, model: string): SelfConfigResult {
|
|
659
|
+
return defaultSelfConfigCore.setModel(ctx, model);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function getSelfConfigModel(ctx: SelfConfigContext): SelfConfigResult {
|
|
663
|
+
return defaultSelfConfigCore.getModel(ctx);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function setSelfConfigAgent(ctx: SelfConfigContext, agent: AgentKind): SelfConfigResult {
|
|
667
|
+
return defaultSelfConfigCore.setAgent(ctx, agent);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function getSelfConfigAgent(ctx: SelfConfigContext): SelfConfigResult {
|
|
671
|
+
return defaultSelfConfigCore.getAgent(ctx);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function setSelfConfigEffort(ctx: SelfConfigContext, effort: EffortLevel): SelfConfigResult {
|
|
675
|
+
return defaultSelfConfigCore.setEffort(ctx, effort);
|
|
248
676
|
}
|
|
249
677
|
|
|
250
678
|
export function getSelfConfigEffort(ctx: SelfConfigContext): SelfConfigResult {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const cfg = getApiTopicConfig(topic.id);
|
|
255
|
-
const agent = currentAgent(topic);
|
|
256
|
-
const registry = getRegistry(agent);
|
|
257
|
-
const fallback = registry.defaultEffort ? `default (${registry.defaultEffort})` : "default (off)";
|
|
258
|
-
const value = cfg?.effort ?? fallback;
|
|
259
|
-
const lock = cfg?.effortLocked ? " [locked by user]" : "";
|
|
260
|
-
return ok(`Effort (agent=${agent}): ${value}${lock}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Persist the topic's sole pending one-shot continuation. The inbox worker
|
|
265
|
-
* claims due rows only when the room is idle and keeps running claims durable
|
|
266
|
-
* until the turn settles.
|
|
267
|
-
*/
|
|
679
|
+
return defaultSelfConfigCore.getEffort(ctx);
|
|
680
|
+
}
|
|
681
|
+
|
|
268
682
|
export function scheduleSelfConfigContinue(
|
|
269
683
|
ctx: SelfConfigContext,
|
|
270
684
|
delaySeconds: number,
|
|
271
685
|
message: string,
|
|
272
686
|
nowMs = Date.now(),
|
|
273
687
|
): SelfConfigResult {
|
|
274
|
-
|
|
275
|
-
if (isResult(topic)) return topic;
|
|
276
|
-
if (
|
|
277
|
-
!Number.isInteger(delaySeconds) ||
|
|
278
|
-
delaySeconds < 1 ||
|
|
279
|
-
delaySeconds > SELF_SCHEDULE_MAX_DELAY_SECONDS
|
|
280
|
-
) {
|
|
281
|
-
return err(`delay_seconds must be an integer from 1 to ${SELF_SCHEDULE_MAX_DELAY_SECONDS}.`);
|
|
282
|
-
}
|
|
283
|
-
const cleanMessage = message.trim();
|
|
284
|
-
if (!cleanMessage) return err("message is required.");
|
|
285
|
-
if (cleanMessage.length > SELF_SCHEDULE_MAX_MESSAGE_LENGTH) {
|
|
286
|
-
return err(`message must be ${SELF_SCHEDULE_MAX_MESSAGE_LENGTH} characters or fewer.`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const deliverAtMs = nowMs + delaySeconds * 1000;
|
|
290
|
-
const created = createPendingSelfSchedule({
|
|
291
|
-
topicId: topic.id,
|
|
292
|
-
userId: ctx.userId,
|
|
293
|
-
message: cleanMessage,
|
|
294
|
-
deliverAt: deliverAtMs,
|
|
295
|
-
now: nowMs,
|
|
296
|
-
});
|
|
297
|
-
if (!created.ok) {
|
|
298
|
-
return err(
|
|
299
|
-
`This topic already has a pending self-schedule (${created.existing.id}) for ` +
|
|
300
|
-
`${new Date(created.existing.deliverAt).toISOString()}. ` +
|
|
301
|
-
"Use update_self_schedule or cancel_self_schedule instead of creating another one.",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
const deliverAt = new Date(deliverAtMs).toISOString();
|
|
305
|
-
return ok(
|
|
306
|
-
`Scheduled this topic to resume in ${delaySeconds} seconds at ${deliverAt}. ` +
|
|
307
|
-
`Schedule ID: ${created.schedule.id}. ` +
|
|
308
|
-
`The continuation is durable across node restarts (delivery granularity is about 5 seconds).`,
|
|
309
|
-
);
|
|
688
|
+
return defaultSelfConfigCore.scheduleContinue(ctx, delaySeconds, message, nowMs);
|
|
310
689
|
}
|
|
311
690
|
|
|
312
691
|
export function getSelfConfigSchedule(
|
|
313
692
|
ctx: SelfConfigContext,
|
|
314
693
|
nowMs = Date.now(),
|
|
315
694
|
): SelfConfigResult {
|
|
316
|
-
|
|
317
|
-
if (isResult(topic)) return topic;
|
|
318
|
-
const schedule = getPendingSelfSchedule(topic.id);
|
|
319
|
-
if (!schedule) return ok("This topic has no pending self-schedule.");
|
|
320
|
-
const remainingSeconds = Math.max(0, Math.ceil((schedule.deliverAt - nowMs) / 1000));
|
|
321
|
-
return ok(
|
|
322
|
-
[
|
|
323
|
-
`Pending self-schedule: ${schedule.id}`,
|
|
324
|
-
`Deliver at: ${new Date(schedule.deliverAt).toISOString()} (in about ${remainingSeconds} seconds)`,
|
|
325
|
-
`Message: ${schedule.message}`,
|
|
326
|
-
].join("\n"),
|
|
327
|
-
);
|
|
695
|
+
return defaultSelfConfigCore.getSchedule(ctx, nowMs);
|
|
328
696
|
}
|
|
329
697
|
|
|
330
698
|
export function updateSelfConfigSchedule(
|
|
@@ -333,98 +701,26 @@ export function updateSelfConfigSchedule(
|
|
|
333
701
|
updates: { delaySeconds?: number; message?: string },
|
|
334
702
|
nowMs = Date.now(),
|
|
335
703
|
): SelfConfigResult {
|
|
336
|
-
|
|
337
|
-
if (isResult(topic)) return topic;
|
|
338
|
-
if (updates.delaySeconds === undefined && updates.message === undefined) {
|
|
339
|
-
return err("Provide delay_seconds, message, or both.");
|
|
340
|
-
}
|
|
341
|
-
if (
|
|
342
|
-
updates.delaySeconds !== undefined &&
|
|
343
|
-
(!Number.isInteger(updates.delaySeconds) ||
|
|
344
|
-
updates.delaySeconds < 1 ||
|
|
345
|
-
updates.delaySeconds > SELF_SCHEDULE_MAX_DELAY_SECONDS)
|
|
346
|
-
) {
|
|
347
|
-
return err(`delay_seconds must be an integer from 1 to ${SELF_SCHEDULE_MAX_DELAY_SECONDS}.`);
|
|
348
|
-
}
|
|
349
|
-
let cleanMessage: string | undefined;
|
|
350
|
-
if (updates.message !== undefined) {
|
|
351
|
-
cleanMessage = updates.message.trim();
|
|
352
|
-
if (!cleanMessage) return err("message is required when provided.");
|
|
353
|
-
if (cleanMessage.length > SELF_SCHEDULE_MAX_MESSAGE_LENGTH) {
|
|
354
|
-
return err(`message must be ${SELF_SCHEDULE_MAX_MESSAGE_LENGTH} characters or fewer.`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const updated = updatePendingSelfSchedule({
|
|
359
|
-
topicId: topic.id,
|
|
360
|
-
scheduleId,
|
|
361
|
-
message: cleanMessage,
|
|
362
|
-
deliverAt: updates.delaySeconds === undefined ? undefined : nowMs + updates.delaySeconds * 1000,
|
|
363
|
-
now: nowMs,
|
|
364
|
-
});
|
|
365
|
-
if (!updated) {
|
|
366
|
-
return err(
|
|
367
|
-
`Pending self-schedule '${scheduleId}' was not found. It may have been cancelled, replaced, or already started.`,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
return ok(
|
|
371
|
-
`Updated self-schedule ${updated.id}. It will resume at ${new Date(updated.deliverAt).toISOString()}.`,
|
|
372
|
-
);
|
|
704
|
+
return defaultSelfConfigCore.updateSchedule(ctx, scheduleId, updates, nowMs);
|
|
373
705
|
}
|
|
374
706
|
|
|
375
707
|
export function cancelSelfConfigSchedule(
|
|
376
708
|
ctx: SelfConfigContext,
|
|
377
709
|
scheduleId: string,
|
|
378
710
|
): SelfConfigResult {
|
|
379
|
-
|
|
380
|
-
if (isResult(topic)) return topic;
|
|
381
|
-
if (!cancelPendingSelfSchedule(topic.id, scheduleId)) {
|
|
382
|
-
return err(
|
|
383
|
-
`Pending self-schedule '${scheduleId}' was not found. It may have been cancelled, replaced, or already started.`,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
return ok(`Cancelled self-schedule ${scheduleId}.`);
|
|
711
|
+
return defaultSelfConfigCore.cancelSchedule(ctx, scheduleId);
|
|
387
712
|
}
|
|
388
713
|
|
|
389
|
-
export
|
|
714
|
+
export function spawnSelfConfigTopic(
|
|
390
715
|
ctx: SelfConfigContext,
|
|
391
716
|
name?: string,
|
|
392
717
|
): Promise<SelfConfigResult> {
|
|
393
|
-
|
|
394
|
-
if (isResult(topic)) return topic;
|
|
395
|
-
|
|
396
|
-
let derived: TopicDto | null;
|
|
397
|
-
try {
|
|
398
|
-
derived = await createDerivedTopic(topic.id, ctx.userId, false, {
|
|
399
|
-
name,
|
|
400
|
-
});
|
|
401
|
-
} catch (e) {
|
|
402
|
-
if (e instanceof TopicTitleConflictError) return err(`${e.message} — pick a different name.`);
|
|
403
|
-
throw e;
|
|
404
|
-
}
|
|
405
|
-
if (!derived) return err("Failed to spawn topic (source not found or permission denied).");
|
|
406
|
-
return ok(`Spawned new topic "${derived.title}".\nLink: ${topicMarkdownLink(derived.id)}`);
|
|
718
|
+
return defaultSelfConfigCore.spawnTopic(ctx, name);
|
|
407
719
|
}
|
|
408
720
|
|
|
409
|
-
export
|
|
721
|
+
export function forkSelfConfigTopic(
|
|
410
722
|
ctx: SelfConfigContext,
|
|
411
723
|
name?: string,
|
|
412
724
|
): Promise<SelfConfigResult> {
|
|
413
|
-
|
|
414
|
-
if (isResult(topic)) return topic;
|
|
415
|
-
|
|
416
|
-
let derived: TopicDto | null;
|
|
417
|
-
try {
|
|
418
|
-
derived = await createDerivedTopic(topic.id, ctx.userId, true, {
|
|
419
|
-
name,
|
|
420
|
-
});
|
|
421
|
-
} catch (e) {
|
|
422
|
-
if (e instanceof TopicTitleConflictError) return err(`${e.message} — pick a different name.`);
|
|
423
|
-
if (e instanceof TopicForkCompactionError) return err(e.message);
|
|
424
|
-
throw e;
|
|
425
|
-
}
|
|
426
|
-
if (!derived) return err("Failed to fork topic (source not found or permission denied).");
|
|
427
|
-
return ok(
|
|
428
|
-
`Forked new topic "${derived.title}". History copied.\nLink: ${topicMarkdownLink(derived.id)}`,
|
|
429
|
-
);
|
|
725
|
+
return defaultSelfConfigCore.forkTopic(ctx, name);
|
|
430
726
|
}
|