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.
@@ -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
- sendResult,
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) || 'Codex',
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
- if (!meta.cwd || !existsSync(meta.cwd)) {
141
- await sendAdapterMessage(adapter, binding, {
142
- type: 'assistant',
143
- text: `⚠️ Codex cwd not found: ${meta.cwd || '(missing)'}`,
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 || meta.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: meta.cwd, codexPath: runtimeCodexPath }, message, {
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: meta.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: meta.cwd,
199
+ cwd: agentHome,
204
200
  });
205
201
  }
206
202
  },
207
203
  })
208
- : await this.runTurn({ sessionId: meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath }, message, {
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 sendResult(adapter, nextBinding, inbound, {
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 || meta.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: meta.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: meta.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, sendResult, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
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
- import { buildOpenClawSessionKey, normalizeOpenClawAgentId, resolveOpenClawWorkspace } from './target.mjs';
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 prompt = inbound.action === 'image'
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 sendResult(adapter, binding, inbound, {
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
- sendResult,
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) || 'opencode',
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
- if (!meta.cwd || !existsSync(meta.cwd)) {
138
- await sendAdapterMessage(adapter, binding, {
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 || meta.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: meta.cwd, opencodePath }, message, {
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: meta.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: meta.cwd,
224
+ cwd: agentHome,
229
225
  });
230
226
  }
231
227
  },
232
228
  })
233
- : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, opencodePath }, message, { files });
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 sendResult(adapter, nextBinding, inbound, {
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 || meta.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: meta.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: meta.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