ticlawk 0.1.15 → 0.1.16-dev.2
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 +96 -212
- package/bin/ticlawk.mjs +228 -46
- package/package.json +2 -5
- package/src/adapters/ticlawk/api.mjs +303 -26
- package/src/adapters/ticlawk/credentials.mjs +1 -2
- package/src/adapters/ticlawk/index.mjs +273 -77
- package/src/cli/agent-commands.mjs +829 -0
- package/src/core/adapter-registry.mjs +12 -28
- package/src/core/agent-cli-handlers.mjs +704 -0
- package/src/core/agent-home.mjs +89 -0
- package/src/core/config.mjs +0 -15
- package/src/core/http.mjs +204 -18
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-env.mjs +41 -5
- package/src/core/runtime-support.mjs +47 -30
- package/src/core/ticlawk-control.mjs +7 -6
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +290 -0
- package/src/runtimes/claude-code/index.mjs +34 -34
- package/src/runtimes/claude-code/session.mjs +15 -7
- package/src/runtimes/codex/index.mjs +31 -36
- package/src/runtimes/codex/session.mjs +9 -5
- package/src/runtimes/openclaw/index.mjs +56 -16
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +30 -35
- package/src/runtimes/opencode/session.mjs +11 -2
- package/src/runtimes/pi/index.mjs +30 -36
- package/src/runtimes/pi/session.mjs +8 -2
- package/ticlawk.mjs +37 -10
- package/assets/ticlawk-concept.svg +0 -137
- package/src/adapters/telegram/index.mjs +0 -359
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* and discover local sessions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
8
|
import { basename } from 'node:path';
|
|
10
9
|
import {
|
|
11
10
|
createCodexSession,
|
|
@@ -25,12 +24,15 @@ import {
|
|
|
25
24
|
shouldStreamRuntime,
|
|
26
25
|
createDeltaAggregator,
|
|
27
26
|
sendAdapterMessage,
|
|
28
|
-
|
|
27
|
+
recordActivity,
|
|
29
28
|
reportSubprocessFailure,
|
|
30
29
|
terminalRuntimeFailure,
|
|
31
30
|
updateBindingRuntimeMeta,
|
|
32
31
|
isRuntimeGatewayFailure,
|
|
33
32
|
} from '../../core/runtime-support.mjs';
|
|
33
|
+
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
34
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
35
|
+
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
34
36
|
|
|
35
37
|
export const codexRuntime = {
|
|
36
38
|
name: 'codex',
|
|
@@ -48,23 +50,26 @@ export const codexRuntime = {
|
|
|
48
50
|
};
|
|
49
51
|
},
|
|
50
52
|
|
|
51
|
-
runTurn({ sessionId, cwd, codexPath }, text, opts = {}) {
|
|
53
|
+
runTurn({ sessionId, cwd, codexPath, agentEnv }, text, opts = {}) {
|
|
52
54
|
return runCodexPrompt({
|
|
53
55
|
sessionId,
|
|
54
56
|
cwd,
|
|
55
57
|
message: text,
|
|
56
58
|
codexPath,
|
|
59
|
+
agentEnv,
|
|
57
60
|
input: opts.input,
|
|
58
61
|
timeoutMs: opts.timeoutMs,
|
|
59
62
|
});
|
|
60
63
|
},
|
|
61
64
|
|
|
62
|
-
runTurnStream({ sessionId, cwd, codexPath }, text, opts = {}) {
|
|
65
|
+
runTurnStream({ sessionId, cwd, codexPath, agentEnv }, text, opts = {}) {
|
|
63
66
|
return streamCodexPrompt({
|
|
64
67
|
sessionId,
|
|
65
68
|
cwd,
|
|
66
69
|
message: text,
|
|
67
70
|
codexPath,
|
|
71
|
+
agentEnv,
|
|
72
|
+
developerInstructions: opts.developerInstructions || null,
|
|
68
73
|
input: opts.input,
|
|
69
74
|
timeoutMs: opts.timeoutMs,
|
|
70
75
|
onEvent: opts.onEvent,
|
|
@@ -93,8 +98,6 @@ export const codexRuntime = {
|
|
|
93
98
|
displayName: payload.name || basename(session.cwd) || 'Codex',
|
|
94
99
|
runtimeMeta: {
|
|
95
100
|
sessionId: session.sessionId,
|
|
96
|
-
workdir: session.cwd,
|
|
97
|
-
cwd: session.cwd,
|
|
98
101
|
path: session.path || null,
|
|
99
102
|
runtimePath: codexPath,
|
|
100
103
|
codexPath,
|
|
@@ -104,20 +107,11 @@ export const codexRuntime = {
|
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
if (!requestedCwd) {
|
|
108
|
-
throw new Error('cwd or sessionId is required for codex binding');
|
|
109
|
-
}
|
|
110
|
-
if (!existsSync(requestedCwd)) {
|
|
111
|
-
throw new Error(`codex cwd not found locally: ${requestedCwd}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
110
|
return {
|
|
115
111
|
runtime: this.name,
|
|
116
|
-
displayName: payload.name || basename(requestedCwd)
|
|
112
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'Codex'),
|
|
117
113
|
runtimeMeta: {
|
|
118
114
|
sessionId: null,
|
|
119
|
-
workdir: requestedCwd,
|
|
120
|
-
cwd: requestedCwd,
|
|
121
115
|
path: null,
|
|
122
116
|
runtimePath: codexPath,
|
|
123
117
|
codexPath,
|
|
@@ -136,16 +130,11 @@ export const codexRuntime = {
|
|
|
136
130
|
if (!binding) return false;
|
|
137
131
|
const adapter = ctx.adapter;
|
|
138
132
|
const meta = binding.runtimeMeta || {};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
media: [],
|
|
145
|
-
replyToMessageId: inbound.messageId || null,
|
|
146
|
-
});
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
133
|
+
// slock-style: cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
|
|
134
|
+
// ensureAgentHome creates it + seeds MEMORY.md if missing.
|
|
135
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
136
|
+
displayName: binding.display_name || binding.name || null,
|
|
137
|
+
});
|
|
149
138
|
|
|
150
139
|
const codexInput = inbound.action === 'image'
|
|
151
140
|
? await buildCodexInputFromInbound(inbound, 'codex')
|
|
@@ -163,7 +152,7 @@ export const codexRuntime = {
|
|
|
163
152
|
agent: this.name,
|
|
164
153
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
165
154
|
turnId: turnId || null,
|
|
166
|
-
cwd: cwd ||
|
|
155
|
+
cwd: cwd || agentHome,
|
|
167
156
|
replyToMessageId: inbound.messageId || null,
|
|
168
157
|
event: {
|
|
169
158
|
hook_event_name: 'worker.message.delta',
|
|
@@ -177,9 +166,16 @@ export const codexRuntime = {
|
|
|
177
166
|
try {
|
|
178
167
|
const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
|
|
179
168
|
const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
|
|
169
|
+
const agentEnv = buildAgentRuntimeEnv({
|
|
170
|
+
agentId: binding.id,
|
|
171
|
+
sessionId: meta.sessionId,
|
|
172
|
+
hostId: binding.runtime_host_id,
|
|
173
|
+
});
|
|
174
|
+
const developerInstructions = buildStandingPrompt({ agentId: binding.id });
|
|
180
175
|
const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
|
|
181
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
176
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
182
177
|
input: codexInput,
|
|
178
|
+
developerInstructions,
|
|
183
179
|
onEvent: async (event) => {
|
|
184
180
|
if (event?.type === 'turn.started') {
|
|
185
181
|
await emitWorkerEvent({
|
|
@@ -188,7 +184,7 @@ export const codexRuntime = {
|
|
|
188
184
|
agent: this.name,
|
|
189
185
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
190
186
|
turnId: event.turnId || null,
|
|
191
|
-
cwd:
|
|
187
|
+
cwd: agentHome,
|
|
192
188
|
replyToMessageId: inbound.messageId || null,
|
|
193
189
|
event: {
|
|
194
190
|
hook_event_name: 'worker.turn.start',
|
|
@@ -200,19 +196,18 @@ export const codexRuntime = {
|
|
|
200
196
|
deltaAggregator.push(event.text, {
|
|
201
197
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
202
198
|
turnId: event.turnId || null,
|
|
203
|
-
cwd:
|
|
199
|
+
cwd: agentHome,
|
|
204
200
|
});
|
|
205
201
|
}
|
|
206
202
|
},
|
|
207
203
|
})
|
|
208
|
-
: await this.runTurn({ sessionId: meta.sessionId, cwd:
|
|
204
|
+
: await this.runTurn({ sessionId: meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
209
205
|
input: codexInput,
|
|
210
206
|
});
|
|
211
207
|
|
|
212
208
|
await deltaAggregator.flush();
|
|
213
209
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
214
210
|
sessionId: result?.sessionId || meta.sessionId,
|
|
215
|
-
cwd: result?.cwd || meta.cwd,
|
|
216
211
|
path: result?.path || meta.path || null,
|
|
217
212
|
runtimePath: runtimeCodexPath,
|
|
218
213
|
codexPath: runtimeCodexPath,
|
|
@@ -220,7 +215,7 @@ export const codexRuntime = {
|
|
|
220
215
|
rotatePending: false,
|
|
221
216
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
222
217
|
}, { status: 'connected' });
|
|
223
|
-
await
|
|
218
|
+
await recordActivity(adapter, nextBinding, inbound, {
|
|
224
219
|
...result,
|
|
225
220
|
media: normalizeOutboundMedia(result),
|
|
226
221
|
});
|
|
@@ -230,7 +225,7 @@ export const codexRuntime = {
|
|
|
230
225
|
agent: this.name,
|
|
231
226
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
232
227
|
turnId: result?.turnId || null,
|
|
233
|
-
cwd: result?.cwd ||
|
|
228
|
+
cwd: result?.cwd || agentHome,
|
|
234
229
|
replyToMessageId: inbound.messageId || null,
|
|
235
230
|
event: {
|
|
236
231
|
hook_event_name: 'Stop',
|
|
@@ -261,7 +256,7 @@ export const codexRuntime = {
|
|
|
261
256
|
agent: this.name,
|
|
262
257
|
sessionId: meta.sessionId || binding.id,
|
|
263
258
|
turnId: failureInfo?.turnId || null,
|
|
264
|
-
cwd:
|
|
259
|
+
cwd: agentHome,
|
|
265
260
|
replyToMessageId: inbound.messageId || null,
|
|
266
261
|
event: {
|
|
267
262
|
hook_event_name: 'worker.turn.error',
|
|
@@ -288,7 +283,7 @@ export const codexRuntime = {
|
|
|
288
283
|
binding,
|
|
289
284
|
agent: this.name,
|
|
290
285
|
sessionId: meta.sessionId || binding.id,
|
|
291
|
-
cwd:
|
|
286
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
292
287
|
event: {
|
|
293
288
|
hook_event_name: 'Stop',
|
|
294
289
|
worker_event_name: 'worker.turn.complete',
|
|
@@ -221,13 +221,13 @@ function isSubAgentThread(thread) {
|
|
|
221
221
|
return Boolean(source.subAgent || source.sub_agent);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
export function runCodexPrompt({ sessionId, cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
224
|
+
export function runCodexPrompt({ sessionId, cwd, message, codexPath = null, agentEnv = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
225
225
|
return new Promise((resolve, reject) => {
|
|
226
226
|
const startedAt = Date.now();
|
|
227
227
|
const codexCommand = requireCodexPath(codexPath);
|
|
228
228
|
const child = spawn(codexCommand, buildCodexExecArgs({ sessionId, message }), {
|
|
229
229
|
cwd,
|
|
230
|
-
env: buildRuntimeEnv(),
|
|
230
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
231
231
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
232
232
|
});
|
|
233
233
|
|
|
@@ -344,6 +344,8 @@ export function streamCodexPrompt({
|
|
|
344
344
|
cwd,
|
|
345
345
|
message,
|
|
346
346
|
codexPath = null,
|
|
347
|
+
agentEnv = null,
|
|
348
|
+
developerInstructions = null,
|
|
347
349
|
input = null,
|
|
348
350
|
timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS),
|
|
349
351
|
onEvent,
|
|
@@ -353,7 +355,7 @@ export function streamCodexPrompt({
|
|
|
353
355
|
const codexCommand = requireCodexPath(codexPath);
|
|
354
356
|
const child = spawn(codexCommand, ['app-server'], {
|
|
355
357
|
cwd,
|
|
356
|
-
env: buildRuntimeEnv(),
|
|
358
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
357
359
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
358
360
|
});
|
|
359
361
|
|
|
@@ -598,6 +600,7 @@ export function streamCodexPrompt({
|
|
|
598
600
|
cwd,
|
|
599
601
|
approvalPolicy: 'never',
|
|
600
602
|
sandbox: 'danger-full-access',
|
|
603
|
+
...(developerInstructions ? { developerInstructions } : {}),
|
|
601
604
|
});
|
|
602
605
|
} else {
|
|
603
606
|
const started = await send('thread/start', {
|
|
@@ -605,6 +608,7 @@ export function streamCodexPrompt({
|
|
|
605
608
|
model: process.env.CODEX_MODEL || null,
|
|
606
609
|
approvalPolicy: 'never',
|
|
607
610
|
sandbox: 'danger-full-access',
|
|
611
|
+
...(developerInstructions ? { developerInstructions } : {}),
|
|
608
612
|
});
|
|
609
613
|
rootThreadId = started?.thread?.id || rootThreadId;
|
|
610
614
|
threadPath = started?.thread?.path || threadPath;
|
|
@@ -636,7 +640,7 @@ export function streamCodexPrompt({
|
|
|
636
640
|
});
|
|
637
641
|
}
|
|
638
642
|
|
|
639
|
-
export function createCodexSession({ cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
643
|
+
export function createCodexSession({ cwd, message, codexPath = null, agentEnv = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
640
644
|
return new Promise((resolve, reject) => {
|
|
641
645
|
const startedAt = Date.now();
|
|
642
646
|
const codexCommand = requireCodexPath(codexPath);
|
|
@@ -651,7 +655,7 @@ export function createCodexSession({ cwd, message, codexPath = null, timeoutMs =
|
|
|
651
655
|
],
|
|
652
656
|
{
|
|
653
657
|
cwd,
|
|
654
|
-
env: buildRuntimeEnv(),
|
|
658
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
655
659
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
656
660
|
}
|
|
657
661
|
);
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
2
|
-
import { reportSubprocessFailure, sendAdapterMessage,
|
|
2
|
+
import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
3
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
3
4
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
// Cheap availability probe used by the harness picker. We can't use
|
|
7
|
+
// isGatewayReady() here because the daemon only opens the gateway WS
|
|
8
|
+
// after at least one openclaw binding exists (registerOpenClawChannel),
|
|
9
|
+
// so an empty install would always look "unavailable" and you'd never
|
|
10
|
+
// be able to pick it for the first time. Probing the gateway's plain
|
|
11
|
+
// HTTP /health avoids that chicken-and-egg.
|
|
12
|
+
const OPENCLAW_HEALTH_TIMEOUT_MS = 1500;
|
|
13
|
+
async function probeOpenClawGatewayHealth() {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`http://${GATEWAY_HOST}:${GATEWAY_PORT}/health`, {
|
|
16
|
+
signal: AbortSignal.timeout(OPENCLAW_HEALTH_TIMEOUT_MS),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) return false;
|
|
19
|
+
const body = await res.json().catch(() => null);
|
|
20
|
+
return body?.ok === true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
|
|
5
26
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
27
|
+
|
|
28
|
+
// Tracks which (agentId, sessionKey) pairs already saw the standing
|
|
29
|
+
// prompt this process lifetime. OpenClaw's gateway holds session state
|
|
30
|
+
// out of process, so we err on the side of "inject on first observed
|
|
31
|
+
// turn after daemon restart"; the gateway dedupes redundant context.
|
|
32
|
+
const standingPromptSeen = new Set();
|
|
6
33
|
import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
|
|
7
34
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
35
|
import { extname } from 'node:path';
|
|
@@ -80,18 +107,24 @@ export const openClawRuntime = {
|
|
|
80
107
|
return isGatewayReady();
|
|
81
108
|
},
|
|
82
109
|
|
|
110
|
+
// Health probe for the harness picker. openclaw is gateway-based —
|
|
111
|
+
// "available" means the local openclaw gateway is up and responds to
|
|
112
|
+
// an unauthenticated /health request. Doesn't depend on the daemon
|
|
113
|
+
// having an open WS, so the picker can offer openclaw on a fresh
|
|
114
|
+
// install where no binding exists yet.
|
|
115
|
+
async health() {
|
|
116
|
+
return {
|
|
117
|
+
available: await probeOpenClawGatewayHealth(),
|
|
118
|
+
path: null,
|
|
119
|
+
version: null,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
83
123
|
async resolveBinding(payload) {
|
|
84
124
|
if (!payload?.agentId) {
|
|
85
125
|
throw new Error('agentId is required for openclaw binding');
|
|
86
126
|
}
|
|
87
127
|
const agentId = normalizeOpenClawAgentId(payload.agentId);
|
|
88
|
-
const workspace = String(
|
|
89
|
-
payload.workdir
|
|
90
|
-
|| payload.projectDir
|
|
91
|
-
|| payload.cwd
|
|
92
|
-
|| resolveOpenClawWorkspace(agentId)
|
|
93
|
-
|| '',
|
|
94
|
-
).trim();
|
|
95
128
|
const sessionKey = payload.sessionKey
|
|
96
129
|
? String(payload.sessionKey).trim()
|
|
97
130
|
: buildOpenClawSessionKey(agentId);
|
|
@@ -103,11 +136,6 @@ export const openClawRuntime = {
|
|
|
103
136
|
sessionKey,
|
|
104
137
|
gatewayHost: GATEWAY_HOST,
|
|
105
138
|
gatewayPort: GATEWAY_PORT,
|
|
106
|
-
...(workspace ? {
|
|
107
|
-
workdir: workspace,
|
|
108
|
-
projectDir: workspace,
|
|
109
|
-
cwd: workspace,
|
|
110
|
-
} : {}),
|
|
111
139
|
},
|
|
112
140
|
};
|
|
113
141
|
},
|
|
@@ -125,12 +153,24 @@ export const openClawRuntime = {
|
|
|
125
153
|
if (!binding) return false;
|
|
126
154
|
const adapter = ctx.adapter;
|
|
127
155
|
const meta = binding.runtimeMeta || {};
|
|
128
|
-
const
|
|
156
|
+
const rawPrompt = inbound.action === 'image'
|
|
129
157
|
? await buildOpenClawImagePrompt(inbound)
|
|
130
158
|
: (inbound.text || '').trim();
|
|
131
159
|
const agentId = normalizeOpenClawAgentId(meta.agentId || binding.id);
|
|
132
160
|
const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
|
|
133
161
|
|
|
162
|
+
// Inject the standing prompt on the first observed turn after a
|
|
163
|
+
// daemon start for this (agent, session) pair. OpenClaw has no
|
|
164
|
+
// separate "system prompt" parameter on the gateway, so we prepend
|
|
165
|
+
// exactly once.
|
|
166
|
+
const standingKey = `${agentId}|${sessionId}`;
|
|
167
|
+
let prompt = rawPrompt;
|
|
168
|
+
if (!standingPromptSeen.has(standingKey)) {
|
|
169
|
+
const standing = buildStandingPrompt({ agentId: binding.id });
|
|
170
|
+
prompt = `${standing}\n\n---\n\n${rawPrompt}`;
|
|
171
|
+
standingPromptSeen.add(standingKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
134
174
|
if (inbound.messageId) {
|
|
135
175
|
addInFlight({
|
|
136
176
|
messageId: inbound.messageId,
|
|
@@ -152,7 +192,7 @@ export const openClawRuntime = {
|
|
|
152
192
|
});
|
|
153
193
|
},
|
|
154
194
|
});
|
|
155
|
-
await
|
|
195
|
+
await recordActivity(adapter, binding, inbound, {
|
|
156
196
|
...result,
|
|
157
197
|
media: normalizeOutboundMedia(result),
|
|
158
198
|
});
|
|
@@ -5,42 +5,12 @@
|
|
|
5
5
|
* `agent:<id>:main` session key is derived internally when dispatching.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
|
|
11
|
-
let cachedOpenClawConfig = null;
|
|
12
|
-
|
|
13
|
-
function loadOpenClawConfig() {
|
|
14
|
-
if (cachedOpenClawConfig !== null) return cachedOpenClawConfig;
|
|
15
|
-
const home = process.env.HOME || '';
|
|
16
|
-
const configPath = home ? join(home, '.openclaw', 'openclaw.json') : '';
|
|
17
|
-
if (!configPath || !existsSync(configPath)) {
|
|
18
|
-
cachedOpenClawConfig = {};
|
|
19
|
-
return cachedOpenClawConfig;
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
cachedOpenClawConfig = JSON.parse(readFileSync(configPath, 'utf8')) || {};
|
|
23
|
-
} catch {
|
|
24
|
-
cachedOpenClawConfig = {};
|
|
25
|
-
}
|
|
26
|
-
return cachedOpenClawConfig;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
8
|
export function normalizeOpenClawAgentId(value) {
|
|
30
9
|
const trimmed = String(value || '').trim().toLowerCase();
|
|
31
10
|
if (!trimmed) return 'main';
|
|
32
11
|
return trimmed.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'main';
|
|
33
12
|
}
|
|
34
13
|
|
|
35
|
-
export function resolveOpenClawWorkspace(agentId) {
|
|
36
|
-
const normalizedId = normalizeOpenClawAgentId(agentId);
|
|
37
|
-
const config = loadOpenClawConfig();
|
|
38
|
-
const agents = Array.isArray(config?.agents?.list) ? config.agents.list : [];
|
|
39
|
-
const matched = agents.find((agent) => normalizeOpenClawAgentId(agent?.id || agent?.name || '') === normalizedId);
|
|
40
|
-
const workspace = String(matched?.workspace || '').trim();
|
|
41
|
-
return workspace || '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
14
|
export function buildOpenClawSessionKey(agentId = 'main') {
|
|
45
15
|
return `agent:${normalizeOpenClawAgentId(agentId)}:main`;
|
|
46
16
|
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
|
+
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
13
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
14
|
+
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
12
15
|
import {
|
|
13
16
|
createOpenCodeSession,
|
|
14
17
|
getOpenCodeRuntimeHealth,
|
|
@@ -27,7 +30,7 @@ import {
|
|
|
27
30
|
shouldStreamRuntime,
|
|
28
31
|
createDeltaAggregator,
|
|
29
32
|
sendAdapterMessage,
|
|
30
|
-
|
|
33
|
+
recordActivity,
|
|
31
34
|
reportSubprocessFailure,
|
|
32
35
|
terminalRuntimeFailure,
|
|
33
36
|
updateBindingRuntimeMeta,
|
|
@@ -40,23 +43,27 @@ export const openCodeRuntime = {
|
|
|
40
43
|
return createOpenCodeSession({ cwd, message: text, opencodePath });
|
|
41
44
|
},
|
|
42
45
|
|
|
43
|
-
runTurn({ sessionId, cwd, opencodePath }, text, opts = {}) {
|
|
46
|
+
runTurn({ sessionId, cwd, opencodePath, agentEnv }, text, opts = {}) {
|
|
44
47
|
return runOpenCodePrompt({
|
|
45
48
|
sessionId,
|
|
46
49
|
cwd,
|
|
47
50
|
message: text,
|
|
48
51
|
opencodePath,
|
|
52
|
+
agentEnv,
|
|
53
|
+
standingPrompt: opts.standingPrompt || null,
|
|
49
54
|
files: opts.files,
|
|
50
55
|
timeoutMs: opts.timeoutMs,
|
|
51
56
|
});
|
|
52
57
|
},
|
|
53
58
|
|
|
54
|
-
runTurnStream({ sessionId, cwd, opencodePath }, text, opts = {}) {
|
|
59
|
+
runTurnStream({ sessionId, cwd, opencodePath, agentEnv }, text, opts = {}) {
|
|
55
60
|
return streamOpenCodePrompt({
|
|
56
61
|
sessionId,
|
|
57
62
|
cwd,
|
|
58
63
|
message: text,
|
|
59
64
|
opencodePath,
|
|
65
|
+
agentEnv,
|
|
66
|
+
standingPrompt: opts.standingPrompt || null,
|
|
60
67
|
files: opts.files,
|
|
61
68
|
timeoutMs: opts.timeoutMs,
|
|
62
69
|
onEvent: opts.onEvent,
|
|
@@ -91,8 +98,6 @@ export const openCodeRuntime = {
|
|
|
91
98
|
displayName: payload.name || session.title || basename(requestedCwd) || 'opencode',
|
|
92
99
|
runtimeMeta: {
|
|
93
100
|
sessionId: session.sessionId,
|
|
94
|
-
workdir: requestedCwd,
|
|
95
|
-
cwd: requestedCwd,
|
|
96
101
|
runtimePath: opencodePath,
|
|
97
102
|
opencodePath,
|
|
98
103
|
opencodeVersion,
|
|
@@ -101,20 +106,11 @@ export const openCodeRuntime = {
|
|
|
101
106
|
};
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
if (!requestedCwd) {
|
|
105
|
-
throw new Error('cwd or sessionId is required for opencode binding');
|
|
106
|
-
}
|
|
107
|
-
if (!existsSync(requestedCwd)) {
|
|
108
|
-
throw new Error(`opencode cwd not found locally: ${requestedCwd}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
109
|
return {
|
|
112
110
|
runtime: this.name,
|
|
113
|
-
displayName: payload.name || basename(requestedCwd)
|
|
111
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'opencode'),
|
|
114
112
|
runtimeMeta: {
|
|
115
113
|
sessionId: null,
|
|
116
|
-
workdir: requestedCwd,
|
|
117
|
-
cwd: requestedCwd,
|
|
118
114
|
runtimePath: opencodePath,
|
|
119
115
|
opencodePath,
|
|
120
116
|
opencodeVersion,
|
|
@@ -133,16 +129,9 @@ export const openCodeRuntime = {
|
|
|
133
129
|
const adapter = ctx.adapter;
|
|
134
130
|
const meta = binding.runtimeMeta || {};
|
|
135
131
|
const runtimeOpenCodePath = meta.opencodePath || meta.runtimePath || null;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
type: 'assistant',
|
|
140
|
-
text: `⚠️ opencode cwd not found: ${meta.cwd || '(missing)'}`,
|
|
141
|
-
media: [],
|
|
142
|
-
replyToMessageId: inbound.messageId || null,
|
|
143
|
-
});
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
132
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
133
|
+
displayName: binding.display_name || binding.name || null,
|
|
134
|
+
});
|
|
146
135
|
|
|
147
136
|
// For image inbound, resolve the attached media to local file paths
|
|
148
137
|
// and forward them to opencode via `--file` (mirrors how Codex uses
|
|
@@ -189,7 +178,7 @@ export const openCodeRuntime = {
|
|
|
189
178
|
binding,
|
|
190
179
|
agent: this.name,
|
|
191
180
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
192
|
-
cwd: cwd ||
|
|
181
|
+
cwd: cwd || agentHome,
|
|
193
182
|
replyToMessageId: inbound.messageId || null,
|
|
194
183
|
event: {
|
|
195
184
|
hook_event_name: 'worker.message.delta',
|
|
@@ -204,8 +193,15 @@ export const openCodeRuntime = {
|
|
|
204
193
|
try {
|
|
205
194
|
const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
|
|
206
195
|
const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
|
|
196
|
+
const agentEnv = buildAgentRuntimeEnv({
|
|
197
|
+
agentId: binding.id,
|
|
198
|
+
sessionId: meta.sessionId,
|
|
199
|
+
hostId: binding.runtime_host_id,
|
|
200
|
+
});
|
|
201
|
+
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
207
202
|
const result = shouldStreamRuntime(this.name, this)
|
|
208
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
203
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
204
|
+
standingPrompt,
|
|
209
205
|
files,
|
|
210
206
|
onEvent: async (event) => {
|
|
211
207
|
if (event?.type === 'turn.started') {
|
|
@@ -214,7 +210,7 @@ export const openCodeRuntime = {
|
|
|
214
210
|
binding,
|
|
215
211
|
agent: this.name,
|
|
216
212
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
217
|
-
cwd:
|
|
213
|
+
cwd: agentHome,
|
|
218
214
|
replyToMessageId: inbound.messageId || null,
|
|
219
215
|
event: {
|
|
220
216
|
hook_event_name: 'worker.turn.start',
|
|
@@ -225,25 +221,24 @@ export const openCodeRuntime = {
|
|
|
225
221
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
226
222
|
deltaAggregator.push(event.text, {
|
|
227
223
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
228
|
-
cwd:
|
|
224
|
+
cwd: agentHome,
|
|
229
225
|
});
|
|
230
226
|
}
|
|
231
227
|
},
|
|
232
228
|
})
|
|
233
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
229
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt });
|
|
234
230
|
|
|
235
231
|
await deltaAggregator.flush();
|
|
236
232
|
|
|
237
233
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
238
234
|
sessionId: result?.sessionId || meta.sessionId,
|
|
239
|
-
cwd: result?.cwd || meta.cwd,
|
|
240
235
|
runtimePath: opencodePath,
|
|
241
236
|
opencodePath,
|
|
242
237
|
opencodeVersion,
|
|
243
238
|
rotatePending: false,
|
|
244
239
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
245
240
|
}, { status: 'connected' });
|
|
246
|
-
await
|
|
241
|
+
await recordActivity(adapter, nextBinding, inbound, {
|
|
247
242
|
...result,
|
|
248
243
|
media: normalizeOutboundMedia(result),
|
|
249
244
|
});
|
|
@@ -252,7 +247,7 @@ export const openCodeRuntime = {
|
|
|
252
247
|
binding: nextBinding,
|
|
253
248
|
agent: this.name,
|
|
254
249
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
255
|
-
cwd: result?.cwd ||
|
|
250
|
+
cwd: result?.cwd || agentHome,
|
|
256
251
|
replyToMessageId: inbound.messageId || null,
|
|
257
252
|
event: {
|
|
258
253
|
hook_event_name: 'Stop',
|
|
@@ -268,7 +263,7 @@ export const openCodeRuntime = {
|
|
|
268
263
|
binding,
|
|
269
264
|
agent: this.name,
|
|
270
265
|
sessionId: meta.sessionId || binding.id,
|
|
271
|
-
cwd:
|
|
266
|
+
cwd: agentHome,
|
|
272
267
|
replyToMessageId: inbound.messageId || null,
|
|
273
268
|
event: {
|
|
274
269
|
hook_event_name: 'worker.turn.error',
|
|
@@ -300,7 +295,7 @@ export const openCodeRuntime = {
|
|
|
300
295
|
binding,
|
|
301
296
|
agent: this.name,
|
|
302
297
|
sessionId: meta.sessionId || binding.id,
|
|
303
|
-
cwd:
|
|
298
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
304
299
|
event: {
|
|
305
300
|
hook_event_name: 'Stop',
|
|
306
301
|
worker_event_name: 'worker.turn.complete',
|
|
@@ -208,15 +208,24 @@ export function runOpenCodePrompt({
|
|
|
208
208
|
message,
|
|
209
209
|
files = [],
|
|
210
210
|
opencodePath = null,
|
|
211
|
+
agentEnv = null,
|
|
212
|
+
standingPrompt = null,
|
|
211
213
|
timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
|
|
212
214
|
onEvent,
|
|
213
215
|
}) {
|
|
216
|
+
// opencode has no documented `--system` flag, so we prepend the
|
|
217
|
+
// standing prompt to the first-turn message body. Resumed sessions
|
|
218
|
+
// (sessionId set) skip injection because the model already saw it
|
|
219
|
+
// on the originating turn.
|
|
220
|
+
const finalMessage = standingPrompt && !sessionId
|
|
221
|
+
? `${standingPrompt}\n\n---\n\n${message}`
|
|
222
|
+
: message;
|
|
214
223
|
return new Promise((resolve, reject) => {
|
|
215
224
|
const startedAt = Date.now();
|
|
216
225
|
const opencodeCommand = requireOpenCodePath(opencodePath);
|
|
217
|
-
const child = spawn(opencodeCommand, buildOpenCodeRunArgs({ sessionId, message, files }), {
|
|
226
|
+
const child = spawn(opencodeCommand, buildOpenCodeRunArgs({ sessionId, message: finalMessage, files }), {
|
|
218
227
|
cwd,
|
|
219
|
-
env: buildRuntimeEnv(),
|
|
228
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
220
229
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
221
230
|
});
|
|
222
231
|
|