opencode-gateway 0.1.0 → 0.1.1
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/cli/doctor.js +3 -1
- package/dist/cli/init.js +4 -1
- package/dist/cli/paths.js +1 -1
- package/dist/cli.js +13 -4
- package/dist/config/gateway.d.ts +1 -0
- package/dist/config/gateway.js +2 -1
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +5 -1
- package/dist/cron/runtime.d.ts +24 -5
- package/dist/cron/runtime.js +178 -13
- package/dist/delivery/text.js +1 -1
- package/dist/gateway.js +41 -35
- package/dist/index.js +9 -5
- package/dist/opencode/adapter.d.ts +2 -0
- package/dist/opencode/adapter.js +56 -7
- package/dist/runtime/conversation-coordinator.d.ts +4 -0
- package/dist/runtime/conversation-coordinator.js +22 -0
- package/dist/runtime/executor.d.ts +33 -5
- package/dist/runtime/executor.js +229 -22
- package/dist/runtime/runtime-singleton.d.ts +2 -0
- package/dist/runtime/runtime-singleton.js +28 -0
- package/dist/session/context.js +6 -0
- package/dist/store/migrations.js +15 -1
- package/dist/store/sqlite.d.ts +19 -2
- package/dist/store/sqlite.js +81 -4
- package/dist/tools/channel-target.d.ts +5 -0
- package/dist/tools/channel-target.js +6 -0
- package/dist/tools/cron-run.js +1 -1
- package/dist/tools/cron-upsert.d.ts +2 -1
- package/dist/tools/cron-upsert.js +20 -6
- package/dist/tools/{cron-remove.d.ts → schedule-cancel.d.ts} +1 -1
- package/dist/tools/schedule-cancel.js +12 -0
- package/dist/tools/schedule-format.d.ts +4 -0
- package/dist/tools/schedule-format.js +48 -0
- package/dist/tools/{cron-list.d.ts → schedule-list.d.ts} +1 -1
- package/dist/tools/schedule-list.js +17 -0
- package/dist/tools/schedule-once.d.ts +4 -0
- package/dist/tools/schedule-once.js +43 -0
- package/dist/tools/schedule-status.d.ts +3 -0
- package/dist/tools/schedule-status.js +23 -0
- package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
- package/package.json +4 -4
- package/dist/tools/cron-list.js +0 -34
- package/dist/tools/cron-remove.js +0 -12
package/dist/cli/doctor.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE } from "../config/paths";
|
|
3
|
+
import { GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, resolveGatewayWorkspacePath } from "../config/paths";
|
|
4
4
|
import { parseOpencodeConfig } from "./opencode-config";
|
|
5
5
|
import { pathExists, resolveCliConfigDir } from "./paths";
|
|
6
6
|
export async function runDoctor(options, env) {
|
|
7
7
|
const configDir = resolveCliConfigDir(options, env);
|
|
8
8
|
const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
|
|
9
9
|
const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
|
|
10
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
10
11
|
const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
|
|
11
12
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
12
13
|
console.log("doctor report");
|
|
13
14
|
console.log(` config dir: ${configDir}`);
|
|
14
15
|
console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
|
|
15
16
|
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
17
|
+
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
16
18
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
17
19
|
console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
|
|
18
20
|
console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
|
package/dist/cli/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, } from "../config/paths";
|
|
3
|
+
import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, resolveGatewayWorkspacePath, } from "../config/paths";
|
|
4
4
|
import { createDefaultOpencodeConfig, ensureGatewayPlugin, parseOpencodeConfig, stringifyOpencodeConfig, } from "./opencode-config";
|
|
5
5
|
import { pathExists, resolveCliConfigDir } from "./paths";
|
|
6
6
|
import { buildGatewayConfigTemplate } from "./templates";
|
|
@@ -8,7 +8,9 @@ export async function runInit(options, env) {
|
|
|
8
8
|
const configDir = resolveCliConfigDir(options, env);
|
|
9
9
|
const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
|
|
10
10
|
const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
|
|
11
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
11
12
|
await mkdir(configDir, { recursive: true });
|
|
13
|
+
await mkdir(workspaceDirPath, { recursive: true });
|
|
12
14
|
let opencodeStatus = "already present";
|
|
13
15
|
if (!(await pathExists(opencodeConfigPath))) {
|
|
14
16
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
@@ -32,4 +34,5 @@ export async function runInit(options, env) {
|
|
|
32
34
|
console.log(`config dir: ${configDir}`);
|
|
33
35
|
console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
|
|
34
36
|
console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
|
|
37
|
+
console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
|
|
35
38
|
}
|
package/dist/cli/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
1
|
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { resolveManagedOpencodeConfigDir, resolveOpencodeConfigDir } from "../config/paths";
|
|
5
5
|
export function resolveCliConfigDir(options, env) {
|
package/dist/cli.js
CHANGED
|
@@ -62,9 +62,10 @@ import { join as join2 } from "node:path";
|
|
|
62
62
|
|
|
63
63
|
// src/config/paths.ts
|
|
64
64
|
import { homedir } from "node:os";
|
|
65
|
-
import { join, resolve as resolve2 } from "node:path";
|
|
65
|
+
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
66
66
|
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
67
67
|
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
68
|
+
var GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
68
69
|
function resolveOpencodeConfigDir(env) {
|
|
69
70
|
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
70
71
|
if (explicit && explicit.trim().length > 0) {
|
|
@@ -75,6 +76,9 @@ function resolveOpencodeConfigDir(env) {
|
|
|
75
76
|
function resolveManagedOpencodeConfigDir(env) {
|
|
76
77
|
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
77
78
|
}
|
|
79
|
+
function resolveGatewayWorkspacePath(configPath) {
|
|
80
|
+
return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
|
|
81
|
+
}
|
|
78
82
|
function defaultGatewayStateDbPath(env) {
|
|
79
83
|
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
80
84
|
}
|
|
@@ -153,8 +157,8 @@ function formatError(error) {
|
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
// src/cli/paths.ts
|
|
156
|
-
import { access } from "node:fs/promises";
|
|
157
160
|
import { constants } from "node:fs";
|
|
161
|
+
import { access } from "node:fs/promises";
|
|
158
162
|
import { resolve as resolve3 } from "node:path";
|
|
159
163
|
function resolveCliConfigDir(options, env) {
|
|
160
164
|
if (options.configDir !== null) {
|
|
@@ -179,12 +183,14 @@ async function runDoctor(options, env) {
|
|
|
179
183
|
const configDir = resolveCliConfigDir(options, env);
|
|
180
184
|
const opencodeConfigPath = join2(configDir, OPENCODE_CONFIG_FILE);
|
|
181
185
|
const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
|
|
186
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
182
187
|
const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
|
|
183
188
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
184
189
|
console.log("doctor report");
|
|
185
190
|
console.log(` config dir: ${configDir}`);
|
|
186
191
|
console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
|
|
187
192
|
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
193
|
+
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
188
194
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
189
195
|
console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
|
|
190
196
|
console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
|
|
@@ -231,7 +237,7 @@ async function inspectOpencodeConfig(path) {
|
|
|
231
237
|
|
|
232
238
|
// src/cli/init.ts
|
|
233
239
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
234
|
-
import { dirname, join as join3 } from "node:path";
|
|
240
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
235
241
|
|
|
236
242
|
// src/cli/templates.ts
|
|
237
243
|
function buildGatewayConfigTemplate(stateDbPath) {
|
|
@@ -267,7 +273,9 @@ async function runInit(options, env) {
|
|
|
267
273
|
const configDir = resolveCliConfigDir(options, env);
|
|
268
274
|
const opencodeConfigPath = join3(configDir, OPENCODE_CONFIG_FILE);
|
|
269
275
|
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
276
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
270
277
|
await mkdir(configDir, { recursive: true });
|
|
278
|
+
await mkdir(workspaceDirPath, { recursive: true });
|
|
271
279
|
let opencodeStatus = "already present";
|
|
272
280
|
if (!await pathExists(opencodeConfigPath)) {
|
|
273
281
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
@@ -283,13 +291,14 @@ async function runInit(options, env) {
|
|
|
283
291
|
}
|
|
284
292
|
let gatewayStatus = "already present";
|
|
285
293
|
if (!await pathExists(gatewayConfigPath)) {
|
|
286
|
-
await mkdir(
|
|
294
|
+
await mkdir(dirname2(gatewayConfigPath), { recursive: true });
|
|
287
295
|
await writeFile(gatewayConfigPath, buildGatewayConfigTemplate(defaultGatewayStateDbPath(env)));
|
|
288
296
|
gatewayStatus = "created";
|
|
289
297
|
}
|
|
290
298
|
console.log(`config dir: ${configDir}`);
|
|
291
299
|
console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
|
|
292
300
|
console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
|
|
301
|
+
console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
// src/cli.ts
|
package/dist/config/gateway.d.ts
CHANGED
package/dist/config/gateway.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { parseCronConfig } from "./cron";
|
|
4
|
-
import { defaultGatewayStateDbPath, resolveGatewayConfigPath } from "./paths";
|
|
4
|
+
import { defaultGatewayStateDbPath, resolveGatewayConfigPath, resolveGatewayWorkspacePath } from "./paths";
|
|
5
5
|
import { parseTelegramConfig } from "./telegram";
|
|
6
6
|
export async function loadGatewayConfig(env = process.env) {
|
|
7
7
|
const configPath = resolveGatewayConfigPath(env);
|
|
@@ -15,6 +15,7 @@ export async function loadGatewayConfig(env = process.env) {
|
|
|
15
15
|
configPath,
|
|
16
16
|
stateDbPath,
|
|
17
17
|
mediaRootPath: resolveMediaRootPath(stateDbPath),
|
|
18
|
+
workspaceDirPath: resolveGatewayWorkspacePath(configPath),
|
|
18
19
|
hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
|
|
19
20
|
legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
|
|
20
21
|
mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
2
2
|
export declare const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
3
|
+
export declare const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
3
4
|
type EnvSource = Record<string, string | undefined>;
|
|
4
5
|
export declare function resolveGatewayConfigPath(env: EnvSource): string;
|
|
5
6
|
export declare function resolveOpencodeConfigDir(env: EnvSource): string;
|
|
6
7
|
export declare function resolveManagedOpencodeConfigDir(env: EnvSource): string;
|
|
8
|
+
export declare function resolveGatewayWorkspacePath(configPath: string): string;
|
|
7
9
|
export declare function defaultGatewayStateDbPath(env: EnvSource): string;
|
|
8
10
|
export declare function resolveConfigHome(env: EnvSource): string;
|
|
9
11
|
export declare function resolveDataHome(env: EnvSource): string;
|
package/dist/config/paths.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
export const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
4
4
|
export const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
5
|
+
export const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
5
6
|
export function resolveGatewayConfigPath(env) {
|
|
6
7
|
const explicit = env.OPENCODE_GATEWAY_CONFIG;
|
|
7
8
|
if (explicit && explicit.trim().length > 0) {
|
|
@@ -19,6 +20,9 @@ export function resolveOpencodeConfigDir(env) {
|
|
|
19
20
|
export function resolveManagedOpencodeConfigDir(env) {
|
|
20
21
|
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
21
22
|
}
|
|
23
|
+
export function resolveGatewayWorkspacePath(configPath) {
|
|
24
|
+
return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
|
|
25
|
+
}
|
|
22
26
|
export function defaultGatewayStateDbPath(env) {
|
|
23
27
|
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
24
28
|
}
|
package/dist/cron/runtime.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { BindingLoggerHost, BindingRuntimeReport, GatewayContract } from "../binding";
|
|
1
|
+
import type { BindingDeliveryTarget, BindingLoggerHost, BindingRuntimeReport, GatewayContract } from "../binding";
|
|
2
2
|
import type { CronConfig } from "../config/cron";
|
|
3
3
|
import type { GatewayExecutorLike } from "../runtime/executor";
|
|
4
|
-
import type { CronJobRecord, SqliteStore } from "../store/sqlite";
|
|
4
|
+
import type { CronJobRecord, CronRunRecord, SqliteStore } from "../store/sqlite";
|
|
5
5
|
export type UpsertCronJobInput = {
|
|
6
6
|
id: string;
|
|
7
7
|
schedule: string;
|
|
@@ -11,6 +11,21 @@ export type UpsertCronJobInput = {
|
|
|
11
11
|
deliveryTarget: string | null;
|
|
12
12
|
deliveryTopic: string | null;
|
|
13
13
|
};
|
|
14
|
+
export type ScheduleOnceInput = {
|
|
15
|
+
id: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
delaySeconds: number | null;
|
|
18
|
+
runAtMs: number | null;
|
|
19
|
+
deliveryChannel: string | null;
|
|
20
|
+
deliveryTarget: string | null;
|
|
21
|
+
deliveryTopic: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type ScheduleJobState = "scheduled" | "running" | "succeeded" | "failed" | "abandoned" | "canceled";
|
|
24
|
+
export type ScheduleJobStatus = {
|
|
25
|
+
job: CronJobRecord;
|
|
26
|
+
state: ScheduleJobState;
|
|
27
|
+
runs: CronRunRecord[];
|
|
28
|
+
};
|
|
14
29
|
export declare class GatewayCronRuntime {
|
|
15
30
|
private readonly executor;
|
|
16
31
|
private readonly contract;
|
|
@@ -18,22 +33,26 @@ export declare class GatewayCronRuntime {
|
|
|
18
33
|
private readonly logger;
|
|
19
34
|
private readonly config;
|
|
20
35
|
private readonly effectiveTimeZone;
|
|
36
|
+
private readonly resolveConversationKeyForTarget;
|
|
21
37
|
private readonly runningJobIds;
|
|
22
38
|
private running;
|
|
23
|
-
constructor(executor: GatewayExecutorLike, contract: GatewayContract, store: SqliteStore, logger: BindingLoggerHost, config: CronConfig, effectiveTimeZone: string);
|
|
39
|
+
constructor(executor: GatewayExecutorLike, contract: GatewayContract, store: SqliteStore, logger: BindingLoggerHost, config: CronConfig, effectiveTimeZone: string, resolveConversationKeyForTarget: (target: BindingDeliveryTarget) => string);
|
|
24
40
|
isEnabled(): boolean;
|
|
25
41
|
isRunning(): boolean;
|
|
26
42
|
runningJobs(): number;
|
|
27
43
|
timeZone(): string;
|
|
28
44
|
start(): void;
|
|
29
|
-
listJobs(): CronJobRecord[];
|
|
45
|
+
listJobs(includeTerminal?: boolean): CronJobRecord[];
|
|
46
|
+
getJobStatus(id: string, limit?: number): ScheduleJobStatus;
|
|
30
47
|
upsertJob(input: UpsertCronJobInput): CronJobRecord;
|
|
31
|
-
|
|
48
|
+
scheduleOnce(input: ScheduleOnceInput): CronJobRecord;
|
|
49
|
+
cancelJob(id: string): boolean;
|
|
32
50
|
runNow(id: string): Promise<BindingRuntimeReport>;
|
|
33
51
|
private runLoop;
|
|
34
52
|
reconcileOnce(nowMs?: number): Promise<void>;
|
|
35
53
|
tickOnce(nowMs?: number): Promise<void>;
|
|
36
54
|
private executeJob;
|
|
55
|
+
private appendScheduleResultToTarget;
|
|
37
56
|
private requireJob;
|
|
38
57
|
private rebaseJobs;
|
|
39
58
|
private readStoredEffectiveTimeZone;
|
package/dist/cron/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatError } from "../utils/error";
|
|
2
2
|
const CRON_EFFECTIVE_TIME_ZONE_KEY = "cron.effective_timezone";
|
|
3
3
|
const LEGACY_CRON_TIME_ZONE = "UTC";
|
|
4
|
+
const MAX_STATUS_RUNS = 20;
|
|
4
5
|
export class GatewayCronRuntime {
|
|
5
6
|
executor;
|
|
6
7
|
contract;
|
|
@@ -8,15 +9,17 @@ export class GatewayCronRuntime {
|
|
|
8
9
|
logger;
|
|
9
10
|
config;
|
|
10
11
|
effectiveTimeZone;
|
|
12
|
+
resolveConversationKeyForTarget;
|
|
11
13
|
runningJobIds = new Set();
|
|
12
14
|
running = false;
|
|
13
|
-
constructor(executor, contract, store, logger, config, effectiveTimeZone) {
|
|
15
|
+
constructor(executor, contract, store, logger, config, effectiveTimeZone, resolveConversationKeyForTarget) {
|
|
14
16
|
this.executor = executor;
|
|
15
17
|
this.contract = contract;
|
|
16
18
|
this.store = store;
|
|
17
19
|
this.logger = logger;
|
|
18
20
|
this.config = config;
|
|
19
21
|
this.effectiveTimeZone = effectiveTimeZone;
|
|
22
|
+
this.resolveConversationKeyForTarget = resolveConversationKeyForTarget;
|
|
20
23
|
}
|
|
21
24
|
isEnabled() {
|
|
22
25
|
return this.config.enabled;
|
|
@@ -39,8 +42,21 @@ export class GatewayCronRuntime {
|
|
|
39
42
|
this.running = false;
|
|
40
43
|
});
|
|
41
44
|
}
|
|
42
|
-
listJobs() {
|
|
43
|
-
|
|
45
|
+
listJobs(includeTerminal = false) {
|
|
46
|
+
const jobs = this.store.listCronJobs();
|
|
47
|
+
if (includeTerminal) {
|
|
48
|
+
return jobs;
|
|
49
|
+
}
|
|
50
|
+
return jobs.filter((job) => !isTerminalJob(job));
|
|
51
|
+
}
|
|
52
|
+
getJobStatus(id, limit = 5) {
|
|
53
|
+
const job = this.requireJob(normalizeId(id));
|
|
54
|
+
const runs = this.store.listCronRuns(job.id, clampStatusLimit(limit));
|
|
55
|
+
return {
|
|
56
|
+
job,
|
|
57
|
+
state: deriveJobState(job, runs[0] ?? null),
|
|
58
|
+
runs,
|
|
59
|
+
};
|
|
44
60
|
}
|
|
45
61
|
upsertJob(input) {
|
|
46
62
|
const normalized = normalizeUpsertInput(input);
|
|
@@ -53,13 +69,31 @@ export class GatewayCronRuntime {
|
|
|
53
69
|
});
|
|
54
70
|
return this.requireJob(normalized.id);
|
|
55
71
|
}
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
scheduleOnce(input) {
|
|
73
|
+
const normalized = normalizeOnceInput(input);
|
|
74
|
+
const recordedAtMs = Date.now();
|
|
75
|
+
this.store.upsertCronJob({
|
|
76
|
+
...normalized,
|
|
77
|
+
nextRunAtMs: normalized.runAtMs ?? recordedAtMs,
|
|
78
|
+
recordedAtMs,
|
|
79
|
+
});
|
|
80
|
+
return this.requireJob(normalized.id);
|
|
81
|
+
}
|
|
82
|
+
cancelJob(id) {
|
|
83
|
+
const job = this.store.getCronJob(normalizeId(id));
|
|
84
|
+
if (job === null || !job.enabled) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
this.store.setCronJobEnabled(job.id, false, Date.now());
|
|
88
|
+
return true;
|
|
58
89
|
}
|
|
59
90
|
async runNow(id) {
|
|
60
91
|
const job = this.requireJob(normalizeId(id));
|
|
92
|
+
if (!job.enabled) {
|
|
93
|
+
throw new Error(`schedule job is not active: ${job.id}`);
|
|
94
|
+
}
|
|
61
95
|
if (this.runningJobIds.has(job.id)) {
|
|
62
|
-
throw new Error(`
|
|
96
|
+
throw new Error(`schedule job is already running: ${job.id}`);
|
|
63
97
|
}
|
|
64
98
|
this.runningJobIds.add(job.id);
|
|
65
99
|
try {
|
|
@@ -88,7 +122,7 @@ export class GatewayCronRuntime {
|
|
|
88
122
|
? `rebasing enabled cron jobs from legacy ${LEGACY_CRON_TIME_ZONE} semantics to ${this.effectiveTimeZone}`
|
|
89
123
|
: `cron time zone changed from ${previousTimeZone} to ${this.effectiveTimeZone}; rebasing enabled jobs`;
|
|
90
124
|
this.logger.log("warn", message);
|
|
91
|
-
this.rebaseJobs(this.store.listCronJobs().filter((job) => job.enabled), nowMs);
|
|
125
|
+
this.rebaseJobs(this.store.listCronJobs().filter((job) => job.enabled && job.kind === "cron"), nowMs);
|
|
92
126
|
}
|
|
93
127
|
else {
|
|
94
128
|
this.rebaseJobs(this.store.listOverdueCronJobs(nowMs), nowMs);
|
|
@@ -108,7 +142,7 @@ export class GatewayCronRuntime {
|
|
|
108
142
|
this.runningJobIds.add(job.id);
|
|
109
143
|
void this.executeJob(job, job.nextRunAtMs, nowMs)
|
|
110
144
|
.catch((error) => {
|
|
111
|
-
this.logger.log("error", `
|
|
145
|
+
this.logger.log("error", `schedule job ${job.id} failed: ${formatError(error)}`);
|
|
112
146
|
})
|
|
113
147
|
.finally(() => {
|
|
114
148
|
this.runningJobIds.delete(job.id);
|
|
@@ -120,26 +154,60 @@ export class GatewayCronRuntime {
|
|
|
120
154
|
}
|
|
121
155
|
async executeJob(job, scheduledForMs, nextRunBaseMs) {
|
|
122
156
|
const startedAtMs = Date.now();
|
|
123
|
-
if (nextRunBaseMs !== null) {
|
|
157
|
+
if (job.kind === "cron" && nextRunBaseMs !== null) {
|
|
124
158
|
const nextRunAtMs = computeNextRunAt(this.contract, job, Math.max(nextRunBaseMs, scheduledForMs), this.effectiveTimeZone);
|
|
125
159
|
this.store.updateCronJobNextRun(job.id, nextRunAtMs, startedAtMs);
|
|
126
160
|
}
|
|
161
|
+
else if (job.kind === "once") {
|
|
162
|
+
this.store.setCronJobEnabled(job.id, false, startedAtMs);
|
|
163
|
+
}
|
|
127
164
|
const runId = this.store.insertCronRun(job.id, scheduledForMs, startedAtMs);
|
|
128
165
|
try {
|
|
129
|
-
const report = await this.executor.
|
|
166
|
+
const report = await this.executor.dispatchScheduledJob({
|
|
167
|
+
jobId: job.id,
|
|
168
|
+
jobKind: job.kind,
|
|
169
|
+
conversationKey: conversationKeyForJob(job),
|
|
170
|
+
prompt: job.prompt,
|
|
171
|
+
replyTarget: toReplyTarget(job),
|
|
172
|
+
});
|
|
130
173
|
this.store.finishCronRun(runId, "succeeded", Date.now(), report.responseText, null);
|
|
174
|
+
await this.appendScheduleResultToTarget(job, scheduledForMs, {
|
|
175
|
+
kind: "success",
|
|
176
|
+
responseText: report.responseText,
|
|
177
|
+
});
|
|
131
178
|
return report;
|
|
132
179
|
}
|
|
133
180
|
catch (error) {
|
|
134
181
|
const message = formatError(error);
|
|
135
182
|
this.store.finishCronRun(runId, "failed", Date.now(), null, message);
|
|
183
|
+
await this.appendScheduleResultToTarget(job, scheduledForMs, {
|
|
184
|
+
kind: "failure",
|
|
185
|
+
errorMessage: message,
|
|
186
|
+
});
|
|
136
187
|
throw error;
|
|
137
188
|
}
|
|
138
189
|
}
|
|
190
|
+
async appendScheduleResultToTarget(job, scheduledForMs, outcome) {
|
|
191
|
+
const replyTarget = toReplyTarget(job);
|
|
192
|
+
if (replyTarget === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await this.executor.appendContextToConversation({
|
|
197
|
+
conversationKey: this.resolveConversationKeyForTarget(replyTarget),
|
|
198
|
+
replyTarget,
|
|
199
|
+
body: formatScheduleContextNote(job, scheduledForMs, outcome),
|
|
200
|
+
recordedAtMs: Date.now(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
this.logger.log("warn", `failed to append schedule result to ${replyTarget.channel}:${replyTarget.target}: ${formatError(error)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
139
207
|
requireJob(id) {
|
|
140
208
|
const job = this.store.getCronJob(id);
|
|
141
209
|
if (job === null) {
|
|
142
|
-
throw new Error(`unknown
|
|
210
|
+
throw new Error(`unknown schedule job: ${id}`);
|
|
143
211
|
}
|
|
144
212
|
return job;
|
|
145
213
|
}
|
|
@@ -186,7 +254,9 @@ function normalizeUpsertInput(input) {
|
|
|
186
254
|
}
|
|
187
255
|
return {
|
|
188
256
|
id,
|
|
257
|
+
kind: "cron",
|
|
189
258
|
schedule,
|
|
259
|
+
runAtMs: null,
|
|
190
260
|
prompt,
|
|
191
261
|
enabled: input.enabled,
|
|
192
262
|
deliveryChannel,
|
|
@@ -196,8 +266,57 @@ function normalizeUpsertInput(input) {
|
|
|
196
266
|
recordedAtMs: 0,
|
|
197
267
|
};
|
|
198
268
|
}
|
|
269
|
+
function normalizeOnceInput(input) {
|
|
270
|
+
const id = normalizeId(input.id);
|
|
271
|
+
const prompt = normalizeRequiredField(input.prompt, "schedule prompt");
|
|
272
|
+
const deliveryChannel = normalizeOptionalField(input.deliveryChannel);
|
|
273
|
+
const deliveryTarget = normalizeOptionalField(input.deliveryTarget);
|
|
274
|
+
const deliveryTopic = normalizeOptionalField(input.deliveryTopic);
|
|
275
|
+
if ((deliveryChannel === null) !== (deliveryTarget === null)) {
|
|
276
|
+
throw new Error("schedule delivery_channel and delivery_target must be provided together");
|
|
277
|
+
}
|
|
278
|
+
if (deliveryChannel === null && deliveryTopic !== null) {
|
|
279
|
+
throw new Error("schedule delivery_topic requires delivery_channel and delivery_target");
|
|
280
|
+
}
|
|
281
|
+
if (deliveryChannel !== null && deliveryChannel !== "telegram") {
|
|
282
|
+
throw new Error(`unsupported schedule delivery channel: ${deliveryChannel}`);
|
|
283
|
+
}
|
|
284
|
+
const runAtMs = resolveOnceRunAt(input);
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
kind: "once",
|
|
288
|
+
schedule: null,
|
|
289
|
+
runAtMs,
|
|
290
|
+
prompt,
|
|
291
|
+
enabled: true,
|
|
292
|
+
deliveryChannel,
|
|
293
|
+
deliveryTarget,
|
|
294
|
+
deliveryTopic,
|
|
295
|
+
nextRunAtMs: runAtMs,
|
|
296
|
+
recordedAtMs: 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function resolveOnceRunAt(input) {
|
|
300
|
+
if (input.delaySeconds === null && input.runAtMs === null) {
|
|
301
|
+
throw new Error("schedule_once requires delay_seconds or run_at_ms");
|
|
302
|
+
}
|
|
303
|
+
if (input.delaySeconds !== null && input.runAtMs !== null) {
|
|
304
|
+
throw new Error("schedule_once accepts only one of delay_seconds or run_at_ms");
|
|
305
|
+
}
|
|
306
|
+
if (input.runAtMs !== null) {
|
|
307
|
+
if (!Number.isSafeInteger(input.runAtMs) || input.runAtMs < 0) {
|
|
308
|
+
throw new Error("schedule run_at_ms must be a non-negative integer");
|
|
309
|
+
}
|
|
310
|
+
return input.runAtMs;
|
|
311
|
+
}
|
|
312
|
+
const delaySeconds = input.delaySeconds ?? 0;
|
|
313
|
+
if (!Number.isSafeInteger(delaySeconds) || delaySeconds < 0) {
|
|
314
|
+
throw new Error("schedule delay_seconds must be a non-negative integer");
|
|
315
|
+
}
|
|
316
|
+
return Date.now() + delaySeconds * 1_000;
|
|
317
|
+
}
|
|
199
318
|
function normalizeId(id) {
|
|
200
|
-
return normalizeRequiredField(id, "
|
|
319
|
+
return normalizeRequiredField(id, "schedule id");
|
|
201
320
|
}
|
|
202
321
|
function normalizeRequiredField(value, field) {
|
|
203
322
|
const trimmed = value.trim();
|
|
@@ -216,7 +335,7 @@ function normalizeOptionalField(value) {
|
|
|
216
335
|
function toBindingCronJobSpec(job) {
|
|
217
336
|
return {
|
|
218
337
|
id: job.id,
|
|
219
|
-
schedule: job.schedule,
|
|
338
|
+
schedule: normalizeRequiredField(job.schedule ?? "", "cron schedule"),
|
|
220
339
|
prompt: job.prompt,
|
|
221
340
|
deliveryChannel: job.deliveryChannel,
|
|
222
341
|
deliveryTarget: job.deliveryTarget,
|
|
@@ -235,3 +354,49 @@ function sleep(durationMs) {
|
|
|
235
354
|
setTimeout(resolve, durationMs);
|
|
236
355
|
});
|
|
237
356
|
}
|
|
357
|
+
function clampStatusLimit(limit) {
|
|
358
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
359
|
+
throw new Error("schedule_status limit must be a positive integer");
|
|
360
|
+
}
|
|
361
|
+
return Math.min(limit, MAX_STATUS_RUNS);
|
|
362
|
+
}
|
|
363
|
+
function deriveJobState(job, latestRun) {
|
|
364
|
+
if (latestRun?.status === "running") {
|
|
365
|
+
return "running";
|
|
366
|
+
}
|
|
367
|
+
if (job.enabled) {
|
|
368
|
+
return "scheduled";
|
|
369
|
+
}
|
|
370
|
+
if (latestRun !== null) {
|
|
371
|
+
return latestRun.status;
|
|
372
|
+
}
|
|
373
|
+
return "canceled";
|
|
374
|
+
}
|
|
375
|
+
function isTerminalJob(job) {
|
|
376
|
+
return !job.enabled;
|
|
377
|
+
}
|
|
378
|
+
function toReplyTarget(job) {
|
|
379
|
+
if (job.deliveryChannel === null || job.deliveryTarget === null) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
channel: job.deliveryChannel,
|
|
384
|
+
target: job.deliveryTarget,
|
|
385
|
+
topic: job.deliveryTopic,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function conversationKeyForJob(job) {
|
|
389
|
+
return job.kind === "cron" ? `cron:${job.id}` : `once:${job.id}`;
|
|
390
|
+
}
|
|
391
|
+
function formatScheduleContextNote(job, scheduledForMs, outcome) {
|
|
392
|
+
const header = [
|
|
393
|
+
"[Gateway schedule result]",
|
|
394
|
+
`job_id=${job.id}`,
|
|
395
|
+
`job_kind=${job.kind}`,
|
|
396
|
+
`scheduled_for_ms=${scheduledForMs}`,
|
|
397
|
+
];
|
|
398
|
+
if (outcome.kind === "success") {
|
|
399
|
+
return [...header, "status=succeeded", "", outcome.responseText].join("\n");
|
|
400
|
+
}
|
|
401
|
+
return [...header, "status=failed", "", outcome.errorMessage].join("\n");
|
|
402
|
+
}
|
package/dist/delivery/text.js
CHANGED
|
@@ -114,7 +114,7 @@ class ProgressiveTextDeliverySession {
|
|
|
114
114
|
this.telegramSupport.startTyping(this.target);
|
|
115
115
|
}
|
|
116
116
|
async preview(text) {
|
|
117
|
-
if (this.previewFailed || this.closed) {
|
|
117
|
+
if (this.previewFailed || this.closed || text.trim().length === 0) {
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
const runPreview = async () => {
|
package/dist/gateway.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
1
2
|
import { loadGatewayConfig } from "./config/gateway";
|
|
2
3
|
import { GatewayCronRuntime } from "./cron/runtime";
|
|
3
4
|
import { TelegramProgressiveSupport } from "./delivery/telegram";
|
|
@@ -13,7 +14,9 @@ import { createQuestionClient } from "./questions/client";
|
|
|
13
14
|
import { GatewayQuestionRuntime } from "./questions/runtime";
|
|
14
15
|
import { GatewayExecutor } from "./runtime/executor";
|
|
15
16
|
import { GatewayMailboxRuntime } from "./runtime/mailbox";
|
|
17
|
+
import { getOrCreateRuntimeSingleton } from "./runtime/runtime-singleton";
|
|
16
18
|
import { GatewaySessionContext } from "./session/context";
|
|
19
|
+
import { resolveConversationKeyForTarget } from "./session/conversation-key";
|
|
17
20
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
18
21
|
import { openSqliteStore } from "./store/sqlite";
|
|
19
22
|
import { TelegramBotClient } from "./telegram/client";
|
|
@@ -56,41 +59,44 @@ export class GatewayPluginRuntime {
|
|
|
56
59
|
}
|
|
57
60
|
export async function createGatewayRuntime(module, input) {
|
|
58
61
|
const config = await loadGatewayConfig();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? new
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
return await getOrCreateRuntimeSingleton(config.configPath, async () => {
|
|
63
|
+
await mkdir(config.workspaceDirPath, { recursive: true });
|
|
64
|
+
const logger = new ConsoleLoggerHost();
|
|
65
|
+
if (config.hasLegacyGatewayTimezone) {
|
|
66
|
+
const suffix = config.legacyGatewayTimezone === null ? "" : ` (${config.legacyGatewayTimezone})`;
|
|
67
|
+
logger.log("warn", `gateway.timezone${suffix} is ignored; use cron.timezone instead`);
|
|
68
|
+
}
|
|
69
|
+
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
70
|
+
const store = await openSqliteStore(config.stateDbPath);
|
|
71
|
+
const sessionContext = new GatewaySessionContext(store);
|
|
72
|
+
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
73
|
+
const telegramMediaStore = config.telegram.enabled && telegramClient !== null
|
|
74
|
+
? new TelegramInboundMediaStore(telegramClient, config.mediaRootPath)
|
|
75
|
+
: null;
|
|
76
|
+
const mailboxRouter = new GatewayMailboxRouter(config.mailbox.routes);
|
|
77
|
+
const opencodeEvents = new OpencodeEventHub();
|
|
78
|
+
const opencode = new OpencodeSdkAdapter(input.client, config.workspaceDirPath);
|
|
79
|
+
const questionClient = createQuestionClient(input.client, input.serverUrl, config.workspaceDirPath);
|
|
80
|
+
const transport = new GatewayTransportHost(telegramClient, store);
|
|
81
|
+
const files = new ChannelFileSender(telegramClient);
|
|
82
|
+
const channelSessions = new ChannelSessionSwitcher(store, sessionContext, mailboxRouter, module, opencode, config.telegram.enabled);
|
|
83
|
+
const questions = new GatewayQuestionRuntime(questionClient, config.workspaceDirPath, store, sessionContext, transport, telegramClient, logger);
|
|
84
|
+
const progressiveSupport = new TelegramProgressiveSupport(telegramClient, store, logger);
|
|
85
|
+
const delivery = new GatewayTextDelivery(transport, store, progressiveSupport);
|
|
86
|
+
const executor = new GatewayExecutor(module, store, opencode, opencodeEvents, delivery, logger);
|
|
87
|
+
const mailbox = new GatewayMailboxRuntime(executor, store, logger, config.mailbox, questions);
|
|
88
|
+
const cron = new GatewayCronRuntime(executor, module, store, logger, config.cron, effectiveCronTimeZone, (target) => resolveConversationKeyForTarget(target, mailboxRouter, module));
|
|
89
|
+
const eventStream = new OpencodeEventStream(input.client, config.workspaceDirPath, opencodeEvents, [questions], logger);
|
|
90
|
+
const telegramPolling = config.telegram.enabled && telegramClient !== null && telegramMediaStore !== null
|
|
91
|
+
? new TelegramPollingService(telegramClient, mailbox, store, logger, config.telegram, mailboxRouter, telegramMediaStore, questions)
|
|
92
|
+
: null;
|
|
93
|
+
const telegram = new GatewayTelegramRuntime(telegramClient, delivery, store, logger, config.telegram, telegramPolling, eventStream);
|
|
94
|
+
eventStream.start();
|
|
95
|
+
cron.start();
|
|
96
|
+
mailbox.start();
|
|
97
|
+
telegram.start();
|
|
98
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext);
|
|
99
|
+
});
|
|
94
100
|
}
|
|
95
101
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
96
102
|
const candidate = config.cron.timezone ?? resolveRuntimeLocalTimeZone();
|