@jc_stack/ez-agents 0.1.0-beta.34 → 0.1.0-beta.37
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/.dockerignore +1 -0
- package/.env.example +5 -16
- package/AGENTS.md +17 -9
- package/CHANGELOG.md +96 -0
- package/Dockerfile +20 -11
- package/README.md +10 -13
- package/THIRD_PARTY_NOTICES.md +7 -4
- package/bin/ez +97 -0
- package/bin/ezenciel-agents-plugin-broker.mjs +16 -0
- package/bin/ezenciel-agents-provision-telegram.mjs +13 -0
- package/bin/ezenciel-agents.mjs +1 -1
- package/compose.yaml +41 -4
- package/docker/entrypoint.sh +1 -1
- package/docker/healthcheck.mjs +12 -4
- package/docker/run.ts +5 -6
- package/docker/smoke.mjs +28 -6
- package/docs/application-channel.md +32 -10
- package/docs/architecture/ai-selection.md +10 -2
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/channel-backend.md +4 -3
- package/docs/docker-runtime.md +98 -11
- package/docs/local-qa.md +2 -2
- package/docs/plugin-guide.md +202 -0
- package/docs/plugins.md +48 -2
- package/docs/releasing.md +13 -0
- package/docs/responsive-channels.md +1 -1
- package/docs/scheduling.md +4 -4
- package/docs/setup.md +11 -5
- package/docs/upgrades.md +46 -23
- package/package.json +5 -5
- package/scripts/smoke-scheduler.ts +10 -3
- package/scripts/smoke.ts +14 -11
- package/src/agent-guidance.ts +6 -9
- package/src/agent-install.ts +28 -7
- package/src/ai-cli.ts +1 -1
- package/src/ai.ts +131 -5
- package/src/application-channel.ts +78 -7
- package/src/application-cli.ts +2 -2
- package/src/approval-cli.ts +14 -7
- package/src/approval.ts +27 -37
- package/src/channel-backend.ts +2 -3
- package/src/config.ts +12 -27
- package/src/control-state.ts +175 -8
- package/src/conversation-menu.ts +6 -17
- package/src/delivery-socket-client.d.mts +4 -0
- package/src/delivery-socket-client.mjs +104 -0
- package/src/delivery-socket.ts +354 -0
- package/src/execution-authority.ts +21 -5
- package/src/executor.ts +81 -24
- package/src/files.ts +38 -11
- package/src/host-executor.ts +33 -17
- package/src/inbox.ts +15 -49
- package/src/index.ts +98 -54
- package/src/install-tools.mjs +97 -22
- package/src/ledger-endpoint.d.mts +2 -0
- package/src/ledger-endpoint.mjs +17 -0
- package/src/menu.ts +8 -3
- package/src/message-history.ts +13 -18
- package/src/message.ts +35 -18
- package/src/plugin-broker.mjs +396 -0
- package/src/plugins/connection.mjs +2 -2
- package/src/plugins/manager.mjs +131 -23
- package/src/plugins/native-tasks.mjs +2 -2
- package/src/prompt-suffix.ts +12 -0
- package/src/react.ts +6 -4
- package/src/runs.ts +156 -243
- package/src/schedule-cli.ts +29 -10
- package/src/scheduled-tasks.ts +53 -10
- package/src/scheduler.ts +20 -7
- package/src/service.ts +3 -3
- package/src/setup.ts +1 -0
- package/src/software-status.ts +36 -4
- package/src/task-executor.ts +2 -2
- package/src/task-workspace.ts +4 -3
- package/src/tasks.ts +1 -1
- package/src/telegram-provisioning-cli.ts +22 -0
- package/src/telegram-provisioning.ts +214 -0
- package/src/updates/control.mjs +77 -7
- package/src/updates/runtime.mjs +28 -8
- package/src/updates/status.mjs +25 -3
- package/src/updates/supervisor.mjs +15 -16
- package/src/version.ts +20 -0
- package/src/workspace.ts +3 -9
- package/templates/failure-review.md +1 -1
- package/templates/maintainer-purpose.md +1 -1
- package/test/agent-guidance.test.ts +14 -80
- package/test/agent-install.test.ts +14 -0
- package/test/ai.test.ts +220 -1
- package/test/application-channel.test.ts +61 -13
- package/test/application-only.test.ts +14 -13
- package/test/channel-backend.test.ts +3 -7
- package/test/channel-delivery.test.ts +4 -1
- package/test/channel-owner.test.ts +6 -3
- package/test/codex-context.test.ts +2 -2
- package/test/config.test.ts +38 -43
- package/test/control-state.test.ts +83 -4
- package/test/conversation-menu.test.ts +2 -4
- package/test/delivery-transport.test.ts +72 -0
- package/test/docker-runtime.test.ts +27 -14
- package/test/engine-handoff.test.ts +7 -6
- package/test/event-sources.test.ts +2 -2
- package/test/execution-authority.test.ts +9 -9
- package/test/executor.test.ts +120 -12
- package/test/failure.test.ts +12 -5
- package/test/files.test.ts +2 -2
- package/test/helpers/ledger.ts +18 -0
- package/test/host-executor.test.ts +87 -3
- package/test/inbox.test.ts +8 -10
- package/test/install-config.test.ts +1 -0
- package/test/install-tools.test.mjs +74 -7
- package/test/intake-relay.test.ts +58 -11
- package/test/message-history.test.ts +39 -47
- package/test/native-tasks.test.ts +1 -1
- package/test/plugin-broker.test.mjs +172 -0
- package/test/plugin-manager.test.mjs +136 -2
- package/test/reaction.test.ts +4 -0
- package/test/relay.test.ts +5 -7
- package/test/repair-policy.test.ts +1 -4
- package/test/schedule-cli.test.ts +14 -9
- package/test/scheduled-tasks.test.ts +29 -6
- package/test/scheduler-host.test.ts +5 -5
- package/test/scheduler-relay.test.ts +15 -9
- package/test/security.test.ts +6 -3
- package/test/software-status.test.ts +21 -3
- package/test/telegram-provisioning.test.ts +160 -0
- package/test/updates.test.mjs +93 -13
- package/test/workspace.test.ts +4 -1
- package/bin/ezenciel-agents-watch.mjs +0 -8
- package/compose.workforce-watch.yaml +0 -33
- package/docker/recovery.ts +0 -11
- package/docs/pagerduty.md +0 -42
- package/docs/workforce-watch.md +0 -101
- package/src/pagerduty.ts +0 -109
- package/src/reply-context.ts +0 -43
- package/src/update-attention.ts +0 -18
- package/src/workforce-watch-cli.ts +0 -14
- package/src/workforce-watch.ts +0 -155
- package/templates/agent/AGENTS.md +0 -5
- package/templates/agent-guidance.md +0 -17
- package/test/pagerduty.test.ts +0 -104
- package/test/update-attention.test.ts +0 -20
- package/test/workforce-watch.test.ts +0 -180
package/src/files.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile, realpath, stat } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, readFile, writeFile, realpath, stat } from 'node:fs/promises'
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
@@ -43,28 +43,55 @@ export const sanitizeFileName = (name: string): string => {
|
|
|
43
43
|
return base || 'file'
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Runtime inbound staging lives under the agent control directory, never the
|
|
47
|
+
// workspace mind folder. The engine resolves the absolute path regardless of
|
|
48
|
+
// its working directory; the prompt carries no workspace-relative prefix.
|
|
46
49
|
export const stageIncomingFile = async (
|
|
47
|
-
|
|
50
|
+
stagingRoot: string,
|
|
48
51
|
fileName: string,
|
|
49
52
|
bytes: Buffer,
|
|
50
53
|
): Promise<{ relativePath: string; fullPath: string; fileType: DetectedFileType }> => {
|
|
51
54
|
const fileType = detectFileType(bytes)
|
|
52
55
|
if (fileType === 'unknown' || bytes.length > 20 * 1024 * 1024)
|
|
53
56
|
throw new Error('Unsupported or oversized attachment')
|
|
54
|
-
const
|
|
55
|
-
await mkdir(
|
|
57
|
+
const attachmentsDir = path.join(stagingRoot, 'attachments')
|
|
58
|
+
await mkdir(attachmentsDir, { recursive: true, mode: 0o700 })
|
|
56
59
|
const sanitized = sanitizeFileName(fileName)
|
|
57
60
|
const targetName = `${randomUUID()}_${sanitized}`
|
|
58
|
-
await workspaceFile(
|
|
59
|
-
const fullPath = path.join(
|
|
61
|
+
await workspaceFile(stagingRoot, 'attachments', false)
|
|
62
|
+
const fullPath = path.join(attachmentsDir, targetName)
|
|
60
63
|
await writeFile(fullPath, bytes, { mode: 0o600 })
|
|
61
64
|
return {
|
|
62
|
-
relativePath: path.join('
|
|
65
|
+
relativePath: path.join('attachments', targetName),
|
|
63
66
|
fullPath,
|
|
64
67
|
fileType,
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
// Reads a staged attachment produced by stageIncomingFile. Relative paths
|
|
72
|
+
// resolve under the first containing root (backward compatible with
|
|
73
|
+
// workspace inbox staging); absolute paths must stay inside one of the roots.
|
|
74
|
+
export const readStagedAttachment = async (roots: (string | undefined)[], file: string): Promise<Buffer> => {
|
|
75
|
+
const candidates = [...new Set(roots.filter((root): root is string => Boolean(root)))]
|
|
76
|
+
if (!path.isAbsolute(file)) {
|
|
77
|
+
for (const root of candidates) {
|
|
78
|
+
try { return await readFile(await workspaceFile(root, file)) } catch {}
|
|
79
|
+
}
|
|
80
|
+
throw new Error('File is outside the workspace')
|
|
81
|
+
}
|
|
82
|
+
const target = await realpath(file).catch(() => { throw new Error('File is outside the workspace') })
|
|
83
|
+
for (const root of candidates) {
|
|
84
|
+
const base = await realpath(root).catch(() => null)
|
|
85
|
+
if (!base) continue
|
|
86
|
+
const relative = path.relative(base, target)
|
|
87
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative) && relative !== '') {
|
|
88
|
+
if (!(await stat(target)).isFile()) throw new Error('Expected a regular file')
|
|
89
|
+
return readFile(target)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error('File is outside the workspace')
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
export const workspaceFile = async (workspace: string, file: string, regular = true): Promise<string> => {
|
|
69
96
|
const root = await realpath(workspace)
|
|
70
97
|
const target = await realpath(path.resolve(root, file))
|
|
@@ -76,12 +103,12 @@ export const workspaceFile = async (workspace: string, file: string, regular = t
|
|
|
76
103
|
|
|
77
104
|
// Canonical native-engine attachment metadata for every inbound channel.
|
|
78
105
|
export const MAX_INCOMING_ATTACHMENT_BYTES = 10 * 1024 * 1024
|
|
79
|
-
export const stageChatAttachment = async (
|
|
106
|
+
export const stageChatAttachment = async (stagingRoot: string, name: string, bytes: Buffer, comment: string) => {
|
|
80
107
|
const type = detectFileType(bytes)
|
|
81
108
|
if (!bytes.length || bytes.length > MAX_INCOMING_ATTACHMENT_BYTES || type === 'unknown' ||
|
|
82
109
|
(type === 'text' && !/\.(txt|md|markdown)$/i.test(name))) throw new Error('Invalid application attachment: unsupported type or size')
|
|
83
|
-
const staged = await stageIncomingFile(
|
|
110
|
+
const staged = await stageIncomingFile(stagingRoot, name, bytes)
|
|
84
111
|
const kind = ['jpeg', 'png', 'webp'].includes(staged.fileType) ? 'image' : 'document'
|
|
85
|
-
return { text: `[Attached ${kind} staged at ${staged.
|
|
86
|
-
attachment: {path: staged.
|
|
112
|
+
return { text: `[Attached ${kind} staged at ${staged.fullPath} (type: ${staged.fileType}, size: ${bytes.length} bytes)]${comment ? `\n\nCaption: ${comment}` : ''}`,
|
|
113
|
+
attachment: {path: staged.fullPath, type: staged.fileType}, fullPath: staged.fullPath }
|
|
87
114
|
}
|
package/src/host-executor.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { installAgentGuidance } from './agent-guidance.js'
|
|
2
1
|
import { redactFailure } from './failure.js'
|
|
3
|
-
import { RunStore } from './runs.js'
|
|
4
2
|
import { Tasks } from './tasks.js'
|
|
5
|
-
import {
|
|
3
|
+
import { ControlStore } from './control-state.js'
|
|
4
|
+
import { authorizeRun, readRun } from './delivery-socket.js'
|
|
5
|
+
import { runPromptSuffix } from './prompt-suffix.js'
|
|
6
6
|
import { mkdir, readFile, writeFile, readdir, rename, rm, appendFile, realpath } from 'node:fs/promises'
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { isHostRunId } from './host-executor-protocol.js'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { startExecutorJob, terminateJob, resolveExecutor, validateCodexProvider, type CodexProviderBinding, type ExecutorOptions } from './executor.js'
|
|
11
11
|
import { parseIsolationClass, type IsolationClass } from './isolation.js'
|
|
12
|
-
import { readModels, validateSelection } from './ai.js'
|
|
12
|
+
import { readModels, validateSelection, validateOpencodeProviders } from './ai.js'
|
|
13
13
|
import type { ChildProcess } from 'node:child_process'
|
|
14
14
|
import { taskWorkspace } from './task-workspace.js'
|
|
15
15
|
import { packageVersion } from './version.js'
|
|
@@ -17,7 +17,7 @@ import { installedPluginVersions } from './software-status.js'
|
|
|
17
17
|
import { processSnapshot } from './process-tree.js'
|
|
18
18
|
|
|
19
19
|
export type PluginNetworkRoute = { revisions:string[]; bindings:{service:string;network:string}[] }
|
|
20
|
-
export type HostBinding = { name: string; workspace: string; controlDir: string; binDir: string; toolsHome?: string; sharedWorkspace?: string; additionalWorkspaces?: string[]; pluginNetworkBindings?: Record<string, PluginNetworkRoute>; pluginFolderRoots?: Record<string,string[]>; codexProviders?: CodexProviderBinding[] }
|
|
20
|
+
export type HostBinding = { name: string; workspace: string; controlDir: string; binDir: string; toolsHome?: string; sharedWorkspace?: string; additionalWorkspaces?: string[]; pluginNetworkBindings?: Record<string, PluginNetworkRoute>; pluginFolderRoots?: Record<string,string[]>; codexProviders?: CodexProviderBinding[]; opencodeProviders?: string[] }
|
|
21
21
|
export type HostInstallation = { cli: string; isolation?: IsolationClass; agents: HostBinding[] }
|
|
22
22
|
|
|
23
23
|
const processStart = async (pid:number) => (await processSnapshot()).get(pid)?.birth
|
|
@@ -35,16 +35,24 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
35
35
|
const sharedWorkspaces = new Map<HostBinding, string>()
|
|
36
36
|
const additionalWorkspaces = new Map<HostBinding, string[]>()
|
|
37
37
|
const catalog = async (agent: HostBinding) => {
|
|
38
|
-
const discovered = await readModels(undefined, undefined, path.join(agent.controlDir, 'cli', 'codex')
|
|
38
|
+
const discovered = await readModels(undefined, undefined, path.join(agent.controlDir, 'cli', 'codex'),
|
|
39
|
+
undefined, undefined, validateOpencodeProviders(agent.opencodeProviders), agent.controlDir)
|
|
39
40
|
const providers = (agent.codexProviders ?? []).map(validateCodexProvider)
|
|
40
|
-
const declared = providers.flatMap(provider => provider.models.map(model =>
|
|
41
|
-
|
|
41
|
+
const declared = providers.flatMap(provider => provider.models.map(model => {
|
|
42
|
+
const native = discovered.find(candidate => candidate.cli === 'codex' && !candidate.provider && candidate.model === model)
|
|
43
|
+
return {cli:'codex',provider:provider.id,model,name:`${provider.name} · ${model}`.slice(0,80),efforts:native?.efforts ?? []}
|
|
44
|
+
}))
|
|
45
|
+
const declaredModels = new Set(declared.map(model => model.model))
|
|
46
|
+
const isReplacedByProvider = (model: {cli:string; model?:string}) =>
|
|
47
|
+
model.model !== undefined && declaredModels.has(model.model) && model.cli === 'codex'
|
|
48
|
+
return [...discovered.filter(model => !isReplacedByProvider(model)), ...declared]
|
|
42
49
|
}
|
|
43
50
|
try {
|
|
44
51
|
for (const agent of installation.agents) {
|
|
45
52
|
if (![agent.workspace,agent.controlDir,agent.binDir].every(path.isAbsolute)) throw new Error('Host bindings require absolute paths')
|
|
46
53
|
const providers=(agent.codexProviders ?? []).map(validateCodexProvider)
|
|
47
54
|
if (new Set(providers.map(provider=>provider.id)).size !== providers.length) throw new Error('Codex provider IDs must be unique')
|
|
55
|
+
validateOpencodeProviders(agent.opencodeProviders)
|
|
48
56
|
if (agent.sharedWorkspace && !path.isAbsolute(agent.sharedWorkspace)) throw new Error('Shared workspace requires an absolute path')
|
|
49
57
|
if (agent.sharedWorkspace) sharedWorkspaces.set(agent, await realpath(agent.sharedWorkspace))
|
|
50
58
|
if (agent.additionalWorkspaces) {
|
|
@@ -71,8 +79,10 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
71
79
|
catch(error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
|
72
80
|
await writeFile(lock,JSON.stringify({pid:process.pid,started:await processStart(process.pid)}),{mode:0o600,flag:'wx'})
|
|
73
81
|
locks.push(lock)
|
|
74
|
-
await
|
|
75
|
-
await
|
|
82
|
+
const models = await catalog(agent)
|
|
83
|
+
await new ControlStore(agent.controlDir, 900000).normalizeProviderBindings(models)
|
|
84
|
+
const modelsFile=path.join(directory,'models.json')
|
|
85
|
+
await writeFile(modelsFile+'.tmp',JSON.stringify(models),{mode:0o600});await rename(modelsFile+'.tmp',modelsFile)
|
|
76
86
|
// A host crash is terminal for a claimed job. Never replay an action.
|
|
77
87
|
for (const file of await readdir(directory)) if (file.endsWith('.running.json')) {
|
|
78
88
|
const base=path.join(directory,file.slice(0,-13))
|
|
@@ -88,27 +98,32 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
88
98
|
if (agent.toolsHome) await (await import('./plugins/workspace-lease.mjs')).recoverNativeLease(agent.toolsHome)
|
|
89
99
|
}
|
|
90
100
|
let catalogAt=Date.now()
|
|
101
|
+
const catalogSeen=new Map<HostBinding,string>()
|
|
91
102
|
while (!signal.aborted) {
|
|
92
103
|
const parent=Number(process.env.EZ_HOST_SUPERVISOR_PID)
|
|
93
104
|
if(parent) { try { process.kill(parent,0) } catch { break } }
|
|
94
105
|
if(Date.now()-catalogAt>30000){
|
|
95
106
|
for(const agent of installation.agents){
|
|
96
|
-
const
|
|
107
|
+
const catalogModels = await catalog(agent)
|
|
108
|
+
const models=JSON.stringify(catalogModels)
|
|
109
|
+
if(catalogSeen.get(agent)===models)continue
|
|
110
|
+
await new ControlStore(agent.controlDir, 900000).normalizeProviderBindings(catalogModels)
|
|
97
111
|
const file=path.join(agent.controlDir,'host-executor/models.json')
|
|
98
112
|
await writeFile(file+'.tmp',models,{mode:0o600});await rename(file+'.tmp',file)
|
|
113
|
+
catalogSeen.set(agent,models)
|
|
99
114
|
}
|
|
100
115
|
catalogAt=Date.now()
|
|
101
116
|
}
|
|
102
117
|
for (const agent of installation.agents) {
|
|
103
118
|
const directory=path.join(agent.controlDir,'host-executor')
|
|
104
|
-
await writeFile(path.join(directory,'heartbeat.tmp'),JSON.stringify({at:Date.now(),pid:process.pid,version:packageVersion,plugins:await installedPluginVersions(agent.toolsHome)}),{mode:0o600})
|
|
119
|
+
await writeFile(path.join(directory,'heartbeat.tmp'),JSON.stringify({at:Date.now(),pid:process.pid,version:packageVersion,platform:process.platform,arch:process.arch,plugins:await installedPluginVersions(agent.toolsHome)}),{mode:0o600})
|
|
105
120
|
await rename(path.join(directory,'heartbeat.tmp'),path.join(directory,'heartbeat.json'))
|
|
106
121
|
for (const file of await readdir(directory)) {
|
|
107
122
|
if (!file.endsWith('.request.json') || !isHostRunId(file.slice(0,-13))) continue
|
|
108
123
|
const id=file.slice(0,-13)
|
|
109
124
|
let run
|
|
110
125
|
try {
|
|
111
|
-
run = await
|
|
126
|
+
run = await readRun(agent.controlDir, id)
|
|
112
127
|
if(id.startsWith('r_schedule_') && !run?.scheduled) throw new Error('Missing scheduled run')
|
|
113
128
|
} catch {
|
|
114
129
|
await appendFile(path.join(directory,id+'.events'),JSON.stringify({stream:'exit',code:1})+'\n',{mode:0o600})
|
|
@@ -131,11 +146,11 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
131
146
|
try { await readFile(base+'.cancel'); throw new Error('Cancelled') } catch(error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
|
132
147
|
const request=JSON.parse(await readFile(base+'.running.json','utf8'))
|
|
133
148
|
if (!Array.isArray(request.texts) || request.texts.some((text:unknown)=>typeof text!=='string')) throw new Error('Invalid job')
|
|
134
|
-
const run = await
|
|
149
|
+
const run = await readRun(agent.controlDir, path.basename(base))
|
|
135
150
|
if (run?.taskId) {
|
|
136
151
|
if (run.status !== 'running') throw new Error('No active task run')
|
|
137
152
|
await new Tasks(agent.controlDir).authorize(run, false)
|
|
138
|
-
} else await
|
|
153
|
+
} else await authorizeRun(agent.controlDir, path.basename(base))
|
|
139
154
|
const opts=request.options as ExecutorOptions
|
|
140
155
|
const cli = opts.cli || installation.cli
|
|
141
156
|
resolveExecutor(cli)
|
|
@@ -149,9 +164,10 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
149
164
|
if (!provider && model && (agent.codexProviders ?? []).some(binding=>binding.models.includes(model)))
|
|
150
165
|
throw new Error('Selected Codex provider is required for this model')
|
|
151
166
|
if (cli !== installation.cli) await validateSelection({id:'selected',name:'Selected model',cli,provider:opts.provider,model,effort:opts.effort},await catalog(agent))
|
|
152
|
-
const options:ExecutorOptions={workspace:run?.scheduled ? await taskWorkspace(agent.
|
|
167
|
+
const options:ExecutorOptions={workspace:run?.scheduled ? await taskWorkspace(agent.controlDir,id) : agent.workspace,controlDir:agent.controlDir,binDir:agent.binDir,toolsHome:agent.toolsHome,sharedWorkspace,additionalWorkspaces:additionalWorkspaces.get(agent),cli,
|
|
153
168
|
runId:path.basename(base),timeoutMs:0,repairEnabled:opts.repairEnabled,
|
|
154
|
-
sessionId:opts.sessionId,isResume:opts.isResume,
|
|
169
|
+
sessionId:opts.sessionId,isResume:opts.isResume,model,effort:opts.effort,provider:opts.provider,codexAutoCompactTokens:opts.codexAutoCompactTokens,codexProvider:provider,
|
|
170
|
+
promptSuffix:runPromptSuffix(run),taskRun:Boolean(run?.taskId),nativeSession:Boolean(run?.scheduled)}
|
|
155
171
|
job=await launch(request.texts,options)
|
|
156
172
|
active.set(base,job.child)
|
|
157
173
|
await writeFile(base+'.process.json',JSON.stringify({pid:job.child.pid}),{mode:0o600})
|
package/src/inbox.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
1
|
import type { Update } from 'grammy/types'
|
|
4
2
|
import { isExecutionChoice, type ExecutionChoice } from './ai.js'
|
|
5
3
|
import { isOwner } from './identity.js'
|
|
@@ -21,58 +19,26 @@ type Entry = { update: Update; receivedAt: number; execution?: ExecutionChoice }
|
|
|
21
19
|
export type InboxBatch = { id: string; entries: Entry[]; status: 'pending' | 'failed' | 'cancelled' }
|
|
22
20
|
type State = { version: 1; seen: number[]; waiting: Entry[]; batches: InboxBatch[] }
|
|
23
21
|
|
|
24
|
-
//
|
|
22
|
+
// Stateless pipe: process memory only. Intake dedup lives in the relay that
|
|
23
|
+
// polls Telegram, so no other process reads this journal. A restart drops
|
|
24
|
+
// unprocessed updates by design; Telegram redelivery is the replay mechanism.
|
|
25
|
+
const statesByDirectory = new Map<string, State>()
|
|
26
|
+
|
|
27
|
+
const stateFor = (directory: string): State => {
|
|
28
|
+
let state = statesByDirectory.get(directory)
|
|
29
|
+
if (!state) { state = { version: 1, seen: [], waiting: [], batches: [] }; statesByDirectory.set(directory, state) }
|
|
30
|
+
return state
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export class InboxStore {
|
|
26
34
|
private lock: Promise<unknown> = Promise.resolve()
|
|
27
|
-
private readonly file: string
|
|
28
35
|
constructor(
|
|
29
36
|
private readonly directory: string,
|
|
30
37
|
private readonly now = Date.now,
|
|
31
|
-
) {
|
|
32
|
-
this.file = join(directory, 'inbox.json')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private async read(): Promise<State> {
|
|
36
|
-
try {
|
|
37
|
-
const state = JSON.parse(await readFile(this.file, 'utf8')) as State
|
|
38
|
-
const entry = (e: Entry) =>
|
|
39
|
-
e && Number.isSafeInteger(e.update?.update_id) && Number.isFinite(e.receivedAt) &&
|
|
40
|
-
(e.execution === undefined || isExecutionChoice(e.execution))
|
|
41
|
-
if (
|
|
42
|
-
state.version !== 1 ||
|
|
43
|
-
!Array.isArray(state.seen) ||
|
|
44
|
-
!state.seen.every(Number.isSafeInteger) ||
|
|
45
|
-
!Array.isArray(state.waiting) ||
|
|
46
|
-
!state.waiting.every(entry) ||
|
|
47
|
-
!Array.isArray(state.batches) ||
|
|
48
|
-
!state.batches.every(
|
|
49
|
-
(b) =>
|
|
50
|
-
/^tg_\d+$/.test(b.id) &&
|
|
51
|
-
['pending', 'failed', 'cancelled'].includes(b.status) &&
|
|
52
|
-
Array.isArray(b.entries) &&
|
|
53
|
-
b.entries.length > 0 &&
|
|
54
|
-
b.entries.every(entry),
|
|
55
|
-
)
|
|
56
|
-
)
|
|
57
|
-
throw new Error('Invalid inbox shape')
|
|
58
|
-
return state
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT')
|
|
61
|
-
return { version: 1, seen: [], waiting: [], batches: [] }
|
|
62
|
-
throw new Error('Inbox unreadable; restore the local journal before accepting messages')
|
|
63
|
-
}
|
|
64
|
-
}
|
|
38
|
+
) {}
|
|
65
39
|
|
|
66
40
|
private change<T>(work: (state: State) => T): Promise<T> {
|
|
67
|
-
const next = this.lock.then(async () =>
|
|
68
|
-
const state = await this.read()
|
|
69
|
-
const result = work(state)
|
|
70
|
-
await mkdir(this.directory, { recursive: true, mode: 0o700 })
|
|
71
|
-
const temporary = `${this.file}.${process.pid}.tmp`
|
|
72
|
-
await writeFile(temporary, JSON.stringify(state), { mode: 0o600 })
|
|
73
|
-
await rename(temporary, this.file)
|
|
74
|
-
return result
|
|
75
|
-
})
|
|
41
|
+
const next = this.lock.then(async () => work(stateFor(this.directory)))
|
|
76
42
|
this.lock = next.catch(() => {})
|
|
77
43
|
return next
|
|
78
44
|
}
|
|
@@ -163,12 +129,12 @@ export class InboxStore {
|
|
|
163
129
|
|
|
164
130
|
async pending(id: string): Promise<boolean> {
|
|
165
131
|
await this.lock
|
|
166
|
-
return (
|
|
132
|
+
return stateFor(this.directory).batches.some((b) => b.id === id && b.status === 'pending')
|
|
167
133
|
}
|
|
168
134
|
|
|
169
135
|
async status(): Promise<{ pending: number; failed: number }> {
|
|
170
136
|
await this.lock
|
|
171
|
-
const state =
|
|
137
|
+
const state = stateFor(this.directory)
|
|
172
138
|
return {
|
|
173
139
|
pending:
|
|
174
140
|
state.waiting.length +
|