ticlawk 0.1.16-dev.4 → 0.1.16-dev.7
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/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +14 -0
- package/src/adapters/ticlawk/index.mjs +63 -60
- package/src/cli/agent-commands.mjs +37 -5
- package/src/core/agent-cli-handlers.mjs +24 -2
- package/src/core/agent-home.mjs +5 -9
- package/src/core/http.mjs +7 -0
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/standing-prompt.mjs +6 -13
- package/src/runtimes/claude-code/index.mjs +21 -99
- package/src/runtimes/codex/index.mjs +1 -1
package/package.json
CHANGED
|
@@ -436,6 +436,20 @@ export async function uploadAgentAttachment({
|
|
|
436
436
|
});
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
export async function uploadAgentAvatar({
|
|
440
|
+
actingAgentId, filename, contentType, dataBase64,
|
|
441
|
+
}) {
|
|
442
|
+
return apiFetch('/api/agent/profile/avatar', {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
acting_as_agent_id: actingAgentId,
|
|
446
|
+
filename,
|
|
447
|
+
content_type: contentType,
|
|
448
|
+
data_base64: dataBase64,
|
|
449
|
+
}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
439
453
|
export async function viewAgentAttachment({ actingAgentId, assetId }) {
|
|
440
454
|
const params = new URLSearchParams();
|
|
441
455
|
params.set('acting_as_agent_id', actingAgentId);
|
|
@@ -27,6 +27,32 @@ function connectError(statusCode, error) {
|
|
|
27
27
|
return { statusCode, body: { ok: false, error } };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Coalesce: returns a wrapped fn that runs at most one invocation at a
|
|
31
|
+
// time. If it's called while an invocation is in flight, the most recent
|
|
32
|
+
// args are stashed and the wrapped fn re-runs exactly once after the
|
|
33
|
+
// current run completes (regardless of how many times it was called
|
|
34
|
+
// during the run). The wrapped fn always returns the in-flight Promise.
|
|
35
|
+
function coalesce(fn) {
|
|
36
|
+
let running = null;
|
|
37
|
+
let pendingArgs = null;
|
|
38
|
+
return function call(...args) {
|
|
39
|
+
if (running) {
|
|
40
|
+
pendingArgs = args;
|
|
41
|
+
return running;
|
|
42
|
+
}
|
|
43
|
+
running = (async () => {
|
|
44
|
+
let currentArgs = args;
|
|
45
|
+
while (true) {
|
|
46
|
+
pendingArgs = null;
|
|
47
|
+
await fn(...currentArgs);
|
|
48
|
+
if (pendingArgs === null) return;
|
|
49
|
+
currentArgs = pendingArgs;
|
|
50
|
+
}
|
|
51
|
+
})().finally(() => { running = null; });
|
|
52
|
+
return running;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
function normalizeInboundMediaAssets(msg) {
|
|
31
57
|
if (!Array.isArray(msg?.media_assets)) return [];
|
|
32
58
|
return msg.media_assets
|
|
@@ -119,7 +145,7 @@ function buildGroupContextBlock(msg) {
|
|
|
119
145
|
// runtime LLM never has to remember the standing prompt to figure out
|
|
120
146
|
// HOW to reply. Codex in particular treats the developerInstructions as
|
|
121
147
|
// background and ignores the chat-send pattern without this per-turn
|
|
122
|
-
// nudge
|
|
148
|
+
// nudge.
|
|
123
149
|
function buildWakePromptText({ envelopeHeader, target, rawText, groupContext }) {
|
|
124
150
|
const body = `${envelopeHeader} ${rawText || ''}`.trim();
|
|
125
151
|
const lines = ['New message received:', '', body];
|
|
@@ -149,8 +175,8 @@ export function normalizeInboundMessage(msg) {
|
|
|
149
175
|
const baseHeader = buildEnvelopeHeader(enriched);
|
|
150
176
|
// Task + reactions suffixes are appended INSIDE the envelope so an
|
|
151
177
|
// agent can see at a glance whether a message is already claimed and
|
|
152
|
-
// who has already acknowledged it
|
|
153
|
-
//
|
|
178
|
+
// who has already acknowledged it: `[task #N status=… assignee=…]` +
|
|
179
|
+
// `[reactions: …]`.
|
|
154
180
|
const taskSuffix = buildTaskSuffix(enriched);
|
|
155
181
|
const reactionsSuffix = buildReactionsSuffix(enriched);
|
|
156
182
|
const header = baseHeader + taskSuffix + reactionsSuffix;
|
|
@@ -257,51 +283,46 @@ function normalizeRuntimeVersion(value) {
|
|
|
257
283
|
return Number.isInteger(value) ? Number(value) : 0;
|
|
258
284
|
}
|
|
259
285
|
|
|
260
|
-
// Build a binding from
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// claim-payload shape returned by claim_pending_deliveries.
|
|
287
|
-
function buildBindingFromDeliveryRow(msg) {
|
|
288
|
-
const agentId = String(msg?.recipient_agent_id || '').trim();
|
|
289
|
-
const runtime = msg?.agent_service_type;
|
|
290
|
-
const hostId = getRuntimeHostIdFromPayload(msg) || undefined;
|
|
286
|
+
// Build a binding from any source row that carries agent metadata.
|
|
287
|
+
// Two source shapes feed in:
|
|
288
|
+
// /api/agents row — id, service_type, meta, …
|
|
289
|
+
// claim_pending_deliveries — recipient_agent_id, agent_service_type,
|
|
290
|
+
// agent_meta, … (prefixed because the
|
|
291
|
+
// delivery row also carries message fields)
|
|
292
|
+
// We try the prefixed names first, then fall through to the bare names,
|
|
293
|
+
// so one builder handles both without the caller having to remember
|
|
294
|
+
// which shape it has.
|
|
295
|
+
function buildBindingFromSource(source) {
|
|
296
|
+
const agentId = String(
|
|
297
|
+
source?.recipient_agent_id
|
|
298
|
+
|| source?.id
|
|
299
|
+
|| source?.agent_id
|
|
300
|
+
|| ''
|
|
301
|
+
).trim();
|
|
302
|
+
const runtime = source?.agent_service_type || source?.service_type;
|
|
303
|
+
const meta = source?.agent_meta ?? source?.meta;
|
|
304
|
+
const runtimeVersion = source?.agent_runtime_version ?? source?.runtime_version;
|
|
305
|
+
const displayName = source?.agent_display_name
|
|
306
|
+
|| source?.agent_name
|
|
307
|
+
|| source?.display_name
|
|
308
|
+
|| source?.name
|
|
309
|
+
|| agentId;
|
|
310
|
+
const status = source?.agent_status || source?.status || 'connected';
|
|
311
|
+
const hostId = getRuntimeHostIdFromPayload(source) || undefined;
|
|
291
312
|
return {
|
|
292
313
|
id: agentId,
|
|
293
314
|
adapter: 'ticlawk',
|
|
294
315
|
targetKey: agentId,
|
|
295
316
|
targetMeta: { agentId, runtime_host_id: hostId },
|
|
296
317
|
runtime_host_id: hostId,
|
|
297
|
-
runtime_host_label: getRuntimeHostLabelFromPayload(
|
|
318
|
+
runtime_host_label: getRuntimeHostLabelFromPayload(source) || undefined,
|
|
298
319
|
runtime,
|
|
299
320
|
runtimeMeta: {
|
|
300
|
-
...projectRuntimeMeta(
|
|
301
|
-
runtimeVersion: normalizeRuntimeVersion(
|
|
321
|
+
...projectRuntimeMeta(meta),
|
|
322
|
+
runtimeVersion: normalizeRuntimeVersion(runtimeVersion),
|
|
302
323
|
},
|
|
303
|
-
displayName
|
|
304
|
-
status
|
|
324
|
+
displayName,
|
|
325
|
+
status,
|
|
305
326
|
};
|
|
306
327
|
}
|
|
307
328
|
|
|
@@ -456,8 +477,6 @@ export function createTiclawkAdapter(ctx) {
|
|
|
456
477
|
let bindingAuditTimer = null;
|
|
457
478
|
let jobsWakeTimer = null;
|
|
458
479
|
let bindingsWakeTimer = null;
|
|
459
|
-
let drainPromise = null;
|
|
460
|
-
let drainRequested = false;
|
|
461
480
|
let lastJobsWakeAt = 0;
|
|
462
481
|
let lastBindingsWakeAt = 0;
|
|
463
482
|
let updateRequired = null;
|
|
@@ -633,7 +652,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
633
652
|
continue;
|
|
634
653
|
}
|
|
635
654
|
|
|
636
|
-
const binding = await ctx.persistBinding(
|
|
655
|
+
const binding = await ctx.persistBinding(buildBindingFromSource(msg));
|
|
637
656
|
if (!binding?.runtime) {
|
|
638
657
|
throw new Error('claimed message missing runtime binding');
|
|
639
658
|
}
|
|
@@ -733,7 +752,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
733
752
|
continue;
|
|
734
753
|
}
|
|
735
754
|
try {
|
|
736
|
-
await ctx.persistBinding(
|
|
755
|
+
await ctx.persistBinding(buildBindingFromSource(agent));
|
|
737
756
|
hydrated += 1;
|
|
738
757
|
} catch (err) {
|
|
739
758
|
debugError('ticlawk', 'binding.hydrate-failed', {
|
|
@@ -883,23 +902,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
883
902
|
return { totalClaimed, iterations };
|
|
884
903
|
}
|
|
885
904
|
|
|
886
|
-
|
|
887
|
-
if (drainPromise) {
|
|
888
|
-
drainRequested = true;
|
|
889
|
-
return drainPromise;
|
|
890
|
-
}
|
|
891
|
-
drainPromise = (async () => {
|
|
892
|
-
let currentReason = reason;
|
|
893
|
-
do {
|
|
894
|
-
drainRequested = false;
|
|
895
|
-
await runDrain(currentReason);
|
|
896
|
-
currentReason = 'drain.requested-again';
|
|
897
|
-
} while (drainRequested);
|
|
898
|
-
})().finally(() => {
|
|
899
|
-
drainPromise = null;
|
|
900
|
-
});
|
|
901
|
-
return drainPromise;
|
|
902
|
-
}
|
|
905
|
+
const requestDrain = coalesce(runDrain);
|
|
903
906
|
|
|
904
907
|
function scheduleDrain(reason) {
|
|
905
908
|
jobsWakeTimer = clearDebounce(jobsWakeTimer);
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* ticlawk server info [--refresh]
|
|
18
18
|
*
|
|
19
19
|
* `ticlawk message send` reads the message body from stdin so heredocs
|
|
20
|
-
* work cleanly
|
|
20
|
+
* work cleanly — quotes, backticks, and code blocks survive without
|
|
21
|
+
* shell-quoting gymnastics.
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -544,13 +545,12 @@ export async function runProfileUpdateCommand(args) {
|
|
|
544
545
|
}
|
|
545
546
|
let avatarUrl = null;
|
|
546
547
|
if (avatarFile) {
|
|
547
|
-
|
|
548
|
-
const upload = await uploadFileViaDaemon(env, avatarFile);
|
|
548
|
+
const upload = await uploadAvatarViaDaemon(env, avatarFile);
|
|
549
549
|
if (!upload.ok) {
|
|
550
550
|
console.error(`avatar upload failed: ${upload.error}`);
|
|
551
551
|
return 1;
|
|
552
552
|
}
|
|
553
|
-
avatarUrl = upload.
|
|
553
|
+
avatarUrl = upload.url;
|
|
554
554
|
}
|
|
555
555
|
const res = await daemonRequest({
|
|
556
556
|
method: 'POST',
|
|
@@ -566,6 +566,34 @@ export async function runProfileUpdateCommand(args) {
|
|
|
566
566
|
return exitFromStatus(res.statusCode);
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
async function uploadAvatarViaDaemon(env, filePath) {
|
|
570
|
+
let stat;
|
|
571
|
+
try { stat = statSync(filePath); } catch (err) {
|
|
572
|
+
return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
|
|
573
|
+
}
|
|
574
|
+
if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
|
|
575
|
+
const contentType = inferContentType(filePath);
|
|
576
|
+
if (!contentType.startsWith('image/')) {
|
|
577
|
+
return { ok: false, error: `avatar must be an image (got content_type ${contentType})` };
|
|
578
|
+
}
|
|
579
|
+
const data = readFileSync(filePath);
|
|
580
|
+
const res = await daemonRequest({
|
|
581
|
+
method: 'POST',
|
|
582
|
+
path: '/agent/profile/avatar',
|
|
583
|
+
headers: commonHeaders(env),
|
|
584
|
+
body: {
|
|
585
|
+
filename: basename(filePath),
|
|
586
|
+
content_type: contentType,
|
|
587
|
+
data_base64: data.toString('base64'),
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
591
|
+
return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
|
|
592
|
+
}
|
|
593
|
+
if (!res.body?.url) return { ok: false, error: 'avatar upload returned no url' };
|
|
594
|
+
return { ok: true, url: res.body.url };
|
|
595
|
+
}
|
|
596
|
+
|
|
569
597
|
async function uploadFileViaDaemon(env, filePath) {
|
|
570
598
|
let stat;
|
|
571
599
|
try { stat = statSync(filePath); } catch (err) {
|
|
@@ -587,7 +615,11 @@ async function uploadFileViaDaemon(env, filePath) {
|
|
|
587
615
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
588
616
|
return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
|
|
589
617
|
}
|
|
590
|
-
|
|
618
|
+
const asset = res.body?.data;
|
|
619
|
+
if (!asset?.asset_id || !asset?.url) {
|
|
620
|
+
return { ok: false, error: 'attachment upload returned no asset' };
|
|
621
|
+
}
|
|
622
|
+
return { ok: true, assetId: asset.asset_id, url: asset.url, expiresAt: asset.expires_at };
|
|
591
623
|
}
|
|
592
624
|
|
|
593
625
|
function inferContentType(filePath) {
|
|
@@ -587,8 +587,8 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
587
587
|
});
|
|
588
588
|
debugLog('agent-cli', 'attachment.upload', {
|
|
589
589
|
actingAgentId,
|
|
590
|
-
asset_id: data?.
|
|
591
|
-
bytes: data?.
|
|
590
|
+
asset_id: data?.data?.asset_id,
|
|
591
|
+
bytes: data?.data?.size_bytes,
|
|
592
592
|
});
|
|
593
593
|
return { status: 200, body: data };
|
|
594
594
|
} catch (err) {
|
|
@@ -596,6 +596,28 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
596
596
|
}
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
export async function handleProfileAvatarUpload(req, body, ctx) {
|
|
600
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
601
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
602
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
603
|
+
if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
|
|
604
|
+
if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
|
|
605
|
+
return { status: 400, body: { error: 'content_type must be image/*' } };
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const data = await api.uploadAgentAvatar({
|
|
609
|
+
actingAgentId,
|
|
610
|
+
filename: body?.filename || 'avatar.bin',
|
|
611
|
+
contentType: body.content_type,
|
|
612
|
+
dataBase64: body.data_base64,
|
|
613
|
+
});
|
|
614
|
+
debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
|
|
615
|
+
return { status: 200, body: data };
|
|
616
|
+
} catch (err) {
|
|
617
|
+
return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
599
621
|
export async function handleAttachmentView(req, query, ctx) {
|
|
600
622
|
const actingAgentId = getActingAgentId(req, query);
|
|
601
623
|
const v = validateActingAgent(actingAgentId, ctx);
|
package/src/core/agent-home.mjs
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-agent
|
|
2
|
+
* Per-agent home directory: ~/.ticlawk/agents/<agent_id>/
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Following slock's daemon model (dist/chunk-M4A5QPUN.js:5079) —
|
|
9
|
-
* dataDir = ~/.slock/agents, agentDataDir = <dataDir>/<id>, and every
|
|
10
|
-
* driver.spawn() receives `workingDirectory: agentDataDir`. One MEMORY.md
|
|
11
|
-
* per agent, lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
4
|
+
* The agent's authoritative workspace. The daemon spawns every runtime
|
|
5
|
+
* with this as cwd; the agent's MEMORY.md, notes/, and any artifacts
|
|
6
|
+
* it produces live here. No project binding — one MEMORY.md per agent,
|
|
7
|
+
* lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
package/src/core/http.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
handleMessageRead,
|
|
12
12
|
handleMessageSearch,
|
|
13
13
|
handleMessageSend,
|
|
14
|
+
handleProfileAvatarUpload,
|
|
14
15
|
handleProfileShow,
|
|
15
16
|
handleProfileUpdate,
|
|
16
17
|
handleReminderCancel,
|
|
@@ -148,6 +149,12 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
148
149
|
const r = await handleProfileUpdate(req, body, cliCtx);
|
|
149
150
|
return writeJson(res, r.status, r.body);
|
|
150
151
|
}
|
|
152
|
+
if (urlNoQuery === '/agent/profile/avatar' && method === 'POST') {
|
|
153
|
+
const body = await readJsonBody(req);
|
|
154
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
155
|
+
const r = await handleProfileAvatarUpload(req, body, cliCtx);
|
|
156
|
+
return writeJson(res, r.status, r.body);
|
|
157
|
+
}
|
|
151
158
|
if (urlNoQuery === '/agent/attachment/upload' && method === 'POST') {
|
|
152
159
|
const body = await readJsonBody(req);
|
|
153
160
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Seed the
|
|
3
|
-
//
|
|
2
|
+
// Seed the per-agent home + MEMORY.md for every paired agent in the
|
|
3
|
+
// linked Supabase project.
|
|
4
4
|
//
|
|
5
5
|
// ~/.ticlawk/agents/<agent_id>/MEMORY.md
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Replaces the Phase-B variant that wrote MEMORY.md into each agent's
|
|
8
|
+
// project workdir. Each agent now has its own home dir as cwd, with
|
|
9
|
+
// MEMORY.md living at the root of that home.
|
|
10
10
|
//
|
|
11
11
|
// Usage:
|
|
12
12
|
// node src/migrate/write-initial-memory.mjs # dry-run
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standing prompt injected into every runtime turn.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* channel → group
|
|
11
|
-
* command surface trimmed to what Ticlawk actually exposes today
|
|
12
|
-
*
|
|
13
|
-
* Adapted sections deliberately preserved verbatim where they encode the
|
|
14
|
-
* etiquette rules that make multi-agent coordination work without
|
|
15
|
-
* runtime-level orchestration. Trim with care; this prompt has been
|
|
16
|
-
* field-calibrated upstream.
|
|
4
|
+
* Encodes the agent's communication contract: how to receive messages,
|
|
5
|
+
* how to reply (always via the `ticlawk` CLI), how to handle ambient
|
|
6
|
+
* vs mention traffic, task lifecycle, and the per-agent home-dir +
|
|
7
|
+
* MEMORY.md workspace convention. Trim with care — the etiquette
|
|
8
|
+
* sections are load-bearing for multi-agent coordination without
|
|
9
|
+
* runtime-level orchestration.
|
|
17
10
|
*/
|
|
18
11
|
|
|
19
12
|
const STANDING_PROMPT = `You are an agent in Ticlawk — a collaborative platform for human-AI
|
|
@@ -12,7 +12,6 @@ import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
|
12
12
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
13
13
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
14
14
|
import {
|
|
15
|
-
createCCSession,
|
|
16
15
|
getClaudeCodeRuntimeHealth,
|
|
17
16
|
runCCPrompt,
|
|
18
17
|
streamCCPrompt,
|
|
@@ -28,7 +27,6 @@ import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
|
28
27
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
29
28
|
import {
|
|
30
29
|
shouldStreamRuntime,
|
|
31
|
-
sendAdapterMessage,
|
|
32
30
|
recordActivity,
|
|
33
31
|
reportSubprocessFailure,
|
|
34
32
|
terminalRuntimeFailure,
|
|
@@ -38,15 +36,9 @@ import {
|
|
|
38
36
|
export const claudeCodeRuntime = {
|
|
39
37
|
name: 'claude_code',
|
|
40
38
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
async createSession({ projectDir, text, claudePath }) {
|
|
45
|
-
return createCCSession({ projectDir, message: text, claudePath });
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
// Run a Claude turn and wait for the final result on stdout. This is
|
|
49
|
-
// the worker-first path used by the adapter for direct reply delivery.
|
|
39
|
+
// Run a Claude turn and wait for the final result on stdout. Used by
|
|
40
|
+
// deliverTurn when streaming is disabled; both fresh sessions
|
|
41
|
+
// (sessionId=null) and resumed sessions go through here.
|
|
50
42
|
runTurn({ sessionId, projectDir, claudePath, agentEnv }, text, opts = {}) {
|
|
51
43
|
return runCCPrompt({
|
|
52
44
|
sessionId,
|
|
@@ -130,109 +122,37 @@ export const claudeCodeRuntime = {
|
|
|
130
122
|
if (!binding) return false;
|
|
131
123
|
const adapter = ctx.adapter;
|
|
132
124
|
const meta = binding.runtimeMeta || {};
|
|
133
|
-
//
|
|
125
|
+
// cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
|
|
134
126
|
const projectDir = ensureAgentHome(binding.id, {
|
|
135
127
|
displayName: binding.display_name || binding.name || null,
|
|
136
128
|
});
|
|
137
|
-
const sessionId = meta.sessionId || binding.id;
|
|
138
129
|
const runtimeClaudePath = meta.claudePath || meta.runtimePath || null;
|
|
139
130
|
|
|
140
131
|
const message = inbound.action === 'image'
|
|
141
132
|
? await buildImageMessageFromInbound(inbound, 'claude-code')
|
|
142
133
|
: inbound.text;
|
|
143
134
|
|
|
135
|
+
// shouldRotate=true means meta.sessionId is missing or invalidated.
|
|
136
|
+
// We pass sessionId=null so `claude` creates a fresh session; the new
|
|
137
|
+
// session_id is captured from stream events and persisted below.
|
|
138
|
+
// Unifying rotate + non-rotate into one path means the standing prompt
|
|
139
|
+
// is always attached, so the agent uses the CLI to reply on every
|
|
140
|
+
// turn — including the first.
|
|
144
141
|
const shouldRotate = !meta.sessionId || meta.rotatePending;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
adapter,
|
|
148
|
-
binding,
|
|
149
|
-
agent: this.name,
|
|
150
|
-
sessionId: sessionId || binding.id,
|
|
151
|
-
cwd: projectDir,
|
|
152
|
-
replyToMessageId: inbound.messageId || null,
|
|
153
|
-
event: {
|
|
154
|
-
hook_event_name: 'worker.turn.start',
|
|
155
|
-
worker_event_name: 'worker.turn.start',
|
|
156
|
-
},
|
|
157
|
-
logger: ctx.logger,
|
|
158
|
-
});
|
|
159
|
-
try {
|
|
160
|
-
const claudePath = requireClaudePath(runtimeClaudePath);
|
|
161
|
-
const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
|
|
162
|
-
const created = await this.createSession({ projectDir, text: message, claudePath });
|
|
163
|
-
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
164
|
-
sessionId: created.sessionId,
|
|
165
|
-
path: null,
|
|
166
|
-
runtimePath: claudePath,
|
|
167
|
-
claudePath,
|
|
168
|
-
claudeVersion,
|
|
169
|
-
rotatePending: false,
|
|
170
|
-
lastRotatedAt: new Date().toISOString(),
|
|
171
|
-
}, { status: 'connected' });
|
|
172
|
-
if (created.resultText && created.resultText.trim()) {
|
|
173
|
-
await sendAdapterMessage(adapter, nextBinding, {
|
|
174
|
-
type: 'assistant',
|
|
175
|
-
text: created.resultText,
|
|
176
|
-
media: [],
|
|
177
|
-
replyToMessageId: inbound.messageId || null,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
await emitWorkerEvent({
|
|
181
|
-
adapter,
|
|
182
|
-
binding: nextBinding,
|
|
183
|
-
agent: this.name,
|
|
184
|
-
sessionId: created.sessionId,
|
|
185
|
-
cwd: projectDir,
|
|
186
|
-
replyToMessageId: inbound.messageId || null,
|
|
187
|
-
event: {
|
|
188
|
-
hook_event_name: 'Stop',
|
|
189
|
-
worker_event_name: 'worker.turn.complete',
|
|
190
|
-
},
|
|
191
|
-
logger: ctx.logger,
|
|
192
|
-
});
|
|
193
|
-
return true;
|
|
194
|
-
} catch (err) {
|
|
195
|
-
await emitWorkerEvent({
|
|
196
|
-
adapter,
|
|
197
|
-
binding,
|
|
198
|
-
agent: this.name,
|
|
199
|
-
sessionId: sessionId || binding.id,
|
|
200
|
-
cwd: projectDir,
|
|
201
|
-
replyToMessageId: inbound.messageId || null,
|
|
202
|
-
event: {
|
|
203
|
-
hook_event_name: 'worker.turn.error',
|
|
204
|
-
worker_event_name: 'worker.turn.error',
|
|
205
|
-
error: err?.message || 'Claude Code failed',
|
|
206
|
-
},
|
|
207
|
-
logger: ctx.logger,
|
|
208
|
-
});
|
|
209
|
-
await reportSubprocessFailure({
|
|
210
|
-
adapter,
|
|
211
|
-
binding,
|
|
212
|
-
inbound,
|
|
213
|
-
runtimeName: 'Claude Code',
|
|
214
|
-
info: err?.info || {
|
|
215
|
-
ok: false,
|
|
216
|
-
kind: 'exit-error',
|
|
217
|
-
errorMessage: err?.message || 'Claude Code failed',
|
|
218
|
-
durationMs: 0,
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
return terminalRuntimeFailure(err?.message || 'Claude Code failed');
|
|
222
|
-
}
|
|
223
|
-
}
|
|
142
|
+
const targetSessionId = shouldRotate ? null : meta.sessionId;
|
|
143
|
+
const errEventSessionId = meta.sessionId || binding.id;
|
|
224
144
|
|
|
225
145
|
try {
|
|
226
146
|
const claudePath = requireClaudePath(runtimeClaudePath);
|
|
227
147
|
const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
|
|
228
148
|
const agentEnv = buildAgentRuntimeEnv({
|
|
229
149
|
agentId: binding.id,
|
|
230
|
-
sessionId,
|
|
150
|
+
sessionId: meta.sessionId,
|
|
231
151
|
hostId: binding.runtime_host_id,
|
|
232
152
|
});
|
|
233
153
|
const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
234
154
|
const result = shouldStreamRuntime(this.name, this)
|
|
235
|
-
? await this.runTurnStream({ sessionId, projectDir, claudePath, agentEnv }, message, {
|
|
155
|
+
? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, {
|
|
236
156
|
appendSystemPrompt,
|
|
237
157
|
onEvent: async (event) => {
|
|
238
158
|
if (event?.type === 'turn.started') {
|
|
@@ -240,7 +160,7 @@ export const claudeCodeRuntime = {
|
|
|
240
160
|
adapter,
|
|
241
161
|
binding,
|
|
242
162
|
agent: this.name,
|
|
243
|
-
sessionId: event.sessionId ||
|
|
163
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
244
164
|
cwd: projectDir,
|
|
245
165
|
replyToMessageId: inbound.messageId || null,
|
|
246
166
|
event: {
|
|
@@ -254,7 +174,7 @@ export const claudeCodeRuntime = {
|
|
|
254
174
|
adapter,
|
|
255
175
|
binding,
|
|
256
176
|
agent: this.name,
|
|
257
|
-
sessionId: event.sessionId ||
|
|
177
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
258
178
|
cwd: projectDir,
|
|
259
179
|
replyToMessageId: inbound.messageId || null,
|
|
260
180
|
event: {
|
|
@@ -267,12 +187,14 @@ export const claudeCodeRuntime = {
|
|
|
267
187
|
}
|
|
268
188
|
},
|
|
269
189
|
})
|
|
270
|
-
: await this.runTurn({ sessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
|
|
190
|
+
: await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
|
|
271
191
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
272
192
|
sessionId: result?.sessionId || meta.sessionId,
|
|
273
193
|
runtimePath: claudePath,
|
|
274
194
|
claudePath,
|
|
275
195
|
claudeVersion,
|
|
196
|
+
rotatePending: false,
|
|
197
|
+
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
276
198
|
}, { status: 'connected' });
|
|
277
199
|
await recordActivity(adapter, nextBinding, inbound, {
|
|
278
200
|
...result,
|
|
@@ -282,7 +204,7 @@ export const claudeCodeRuntime = {
|
|
|
282
204
|
adapter,
|
|
283
205
|
binding: nextBinding,
|
|
284
206
|
agent: this.name,
|
|
285
|
-
sessionId: result?.sessionId ||
|
|
207
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
286
208
|
cwd: projectDir,
|
|
287
209
|
replyToMessageId: inbound.messageId || null,
|
|
288
210
|
event: {
|
|
@@ -297,7 +219,7 @@ export const claudeCodeRuntime = {
|
|
|
297
219
|
adapter,
|
|
298
220
|
binding,
|
|
299
221
|
agent: this.name,
|
|
300
|
-
sessionId,
|
|
222
|
+
sessionId: errEventSessionId,
|
|
301
223
|
cwd: projectDir,
|
|
302
224
|
replyToMessageId: inbound.messageId || null,
|
|
303
225
|
event: {
|
|
@@ -130,7 +130,7 @@ export const codexRuntime = {
|
|
|
130
130
|
if (!binding) return false;
|
|
131
131
|
const adapter = ctx.adapter;
|
|
132
132
|
const meta = binding.runtimeMeta || {};
|
|
133
|
-
//
|
|
133
|
+
// cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
|
|
134
134
|
// ensureAgentHome creates it + seeds MEMORY.md if missing.
|
|
135
135
|
const agentHome = ensureAgentHome(binding.id, {
|
|
136
136
|
displayName: binding.display_name || binding.name || null,
|