@controlflow-ai/daemon 0.1.0

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.
Files changed (67) hide show
  1. package/README.md +360 -0
  2. package/bin/console.js +2 -0
  3. package/bin/daemon.js +2 -0
  4. package/bin/pal.js +2 -0
  5. package/bin/server.js +2 -0
  6. package/package.json +31 -0
  7. package/src/agent-runtime.ts +285 -0
  8. package/src/app.ts +745 -0
  9. package/src/args.ts +54 -0
  10. package/src/artifacts.ts +85 -0
  11. package/src/cli.ts +284 -0
  12. package/src/client.ts +310 -0
  13. package/src/coco.ts +52 -0
  14. package/src/codex.ts +41 -0
  15. package/src/coding-agent-runtime.ts +20 -0
  16. package/src/config.ts +106 -0
  17. package/src/console.ts +349 -0
  18. package/src/daemon-client.ts +91 -0
  19. package/src/daemon.ts +580 -0
  20. package/src/db.ts +2830 -0
  21. package/src/failure-message.ts +17 -0
  22. package/src/format.ts +13 -0
  23. package/src/http.ts +55 -0
  24. package/src/lark/agent-runtime.ts +142 -0
  25. package/src/lark/cli.ts +549 -0
  26. package/src/lark/credentials.ts +105 -0
  27. package/src/lark/daemon-integration.ts +108 -0
  28. package/src/lark/dispatcher.ts +374 -0
  29. package/src/lark/event-router.ts +329 -0
  30. package/src/lark/inbound-events.ts +131 -0
  31. package/src/lark/server-integration.ts +445 -0
  32. package/src/lark/setup.ts +326 -0
  33. package/src/lark/ws-daemon.ts +224 -0
  34. package/src/lark-fixture-diagnostics.ts +56 -0
  35. package/src/lark-fixture.ts +277 -0
  36. package/src/local-api.ts +155 -0
  37. package/src/local-auth.ts +45 -0
  38. package/src/migrations/001_initial.ts +61 -0
  39. package/src/migrations/002_daemon_deliveries.ts +52 -0
  40. package/src/migrations/003_sessions_runs.ts +49 -0
  41. package/src/migrations/004_message_idempotency.ts +21 -0
  42. package/src/migrations/005_artifacts.ts +24 -0
  43. package/src/migrations/006_lark_channel_foundation.ts +119 -0
  44. package/src/migrations/007_agents_a0.ts +17 -0
  45. package/src/migrations/008_b0_chat_history.ts +31 -0
  46. package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
  47. package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
  48. package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
  49. package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
  50. package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
  51. package/src/migrations/014_agents_runtime.ts +10 -0
  52. package/src/migrations/015_agent_runtime_sessions.ts +15 -0
  53. package/src/migrations/016_room_participants.ts +27 -0
  54. package/src/migrations/017_unified_room_delivery.ts +203 -0
  55. package/src/migrations/018_room_display_names.ts +36 -0
  56. package/src/migrations/019_computer_connections.ts +63 -0
  57. package/src/migrations/020_computer_agent_assignments.ts +20 -0
  58. package/src/migrations/021_provider_identity_bindings.ts +32 -0
  59. package/src/migrations.ts +85 -0
  60. package/src/neeko.ts +23 -0
  61. package/src/provider-identity.ts +40 -0
  62. package/src/runtime-registry.ts +41 -0
  63. package/src/server-auth.ts +13 -0
  64. package/src/server.ts +63 -0
  65. package/src/token-file.ts +57 -0
  66. package/src/types.ts +408 -0
  67. package/src/web.ts +565 -0
@@ -0,0 +1,17 @@
1
+ import type { LockClient } from './client.js';
2
+ import type { Message } from './types.js';
3
+
4
+ export async function writeFailureMessage(client: LockClient, message: Message, agent: string, output: string): Promise<void> {
5
+ const content = `daemon failed to run ${agent}:\n${output}`;
6
+ const parentId = message.parent_id ?? message.id;
7
+ const idempotencyKey = `delivery:${message.id}:failure-system-message`;
8
+
9
+ await client.sendMessage({
10
+ chat: message.chat_name,
11
+ parent_id: parentId,
12
+ sender: agent,
13
+ content,
14
+ type: 'system',
15
+ idempotency_key: idempotencyKey,
16
+ });
17
+ }
package/src/format.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Message } from './types.js';
2
+ import { sanitizeProviderIds } from './provider-identity.js';
3
+
4
+ export function formatMessage(message: Message): string {
5
+ const chatName = sanitizeProviderIds(message.chat_name);
6
+ const target = message.parent_id === null ? `#${chatName}` : `#${chatName}:${message.parent_id}`;
7
+ const to = message.recipient ? ` -> @${sanitizeProviderIds(message.recipient)}` : '';
8
+ return `[${message.id} ${target} ${message.created_at}] @${sanitizeProviderIds(message.sender)}${to}: ${sanitizeProviderIds(message.content)}`;
9
+ }
10
+
11
+ export function formatMessages(messages: Message[]): string {
12
+ return messages.map(formatMessage).join('\n');
13
+ }
package/src/http.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { ApiResponse } from './types.js';
2
+
3
+ export class HttpError extends Error {
4
+ readonly status: number;
5
+ readonly code: string;
6
+
7
+ constructor(status: number, code: string, message: string) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ export async function readJson<T extends object>(request: Request): Promise<T> {
15
+ try {
16
+ const value = await request.json();
17
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
18
+ throw new HttpError(400, 'BAD_JSON', 'expected a JSON object');
19
+ }
20
+ return value as T;
21
+ } catch (error) {
22
+ if (error instanceof HttpError) throw error;
23
+ throw new HttpError(400, 'BAD_JSON', 'request body must be valid JSON');
24
+ }
25
+ }
26
+
27
+ export function json<T>(data: T, status = 200): Response {
28
+ const body: ApiResponse<T> = { ok: true, data };
29
+ return Response.json(body, { status });
30
+ }
31
+
32
+ export function failure(error: unknown): Response {
33
+ if (error instanceof HttpError) {
34
+ return Response.json({ ok: false, code: error.code, message: error.message }, { status: error.status });
35
+ }
36
+
37
+ if (error instanceof Error) {
38
+ return Response.json({ ok: false, code: 'ERROR', message: error.message }, { status: 400 });
39
+ }
40
+
41
+ return Response.json({ ok: false, code: 'ERROR', message: 'unknown error' }, { status: 500 });
42
+ }
43
+
44
+ export function numberParam(url: URL, name: string, fallback?: number): number | undefined {
45
+ const value = url.searchParams.get(name);
46
+ if (value === null || value === '') return fallback;
47
+ const parsed = Number(value);
48
+ if (!Number.isFinite(parsed)) throw new HttpError(400, 'BAD_PARAM', `${name} must be a number`);
49
+ return parsed;
50
+ }
51
+
52
+ export function stringParam(url: URL, name: string): string | undefined {
53
+ const value = url.searchParams.get(name);
54
+ return value === null || value === '' ? undefined : value;
55
+ }
@@ -0,0 +1,142 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export interface AgentInvocation {
4
+ /** Chat-scoped identifier so the runtime can keep per-chat state if desired. */
5
+ chatKey: string;
6
+ /** Most-recent batch of messages to feed to the agent. */
7
+ messages: Array<{
8
+ senderOpenId: string;
9
+ text: string;
10
+ larkMessageId: string;
11
+ lockMessageId: number;
12
+ parentLockMessageId: number | null;
13
+ }>;
14
+ /** True when the agent was idle and this is the first batch in a new run. */
15
+ isFresh: boolean;
16
+ /** When true, this batch is a re-delivery after a kill/restart; runtime
17
+ * may want to log it or adjust prompt framing. */
18
+ isRedelivery?: boolean;
19
+ /** Abort signal: when the dispatcher kills a run, this signal is fired.
20
+ * Runtimes that respect it should bail out promptly and reject the
21
+ * promise (any error works). */
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ export interface AgentReply {
26
+ /** Plain text to post back to the chat. Empty string suppresses any send. */
27
+ text: string;
28
+ /** Optional structured metadata for logging / future routing. */
29
+ meta?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface AgentRuntime {
33
+ readonly name: string;
34
+ run(invocation: AgentInvocation): Promise<AgentReply>;
35
+ }
36
+
37
+ /** Trivial runtime: echoes back the concatenated text with an agent prefix. */
38
+ export function makeEchoRuntime(prefix = 'echo'): AgentRuntime {
39
+ return {
40
+ name: `echo:${prefix}`,
41
+ async run(invocation): Promise<AgentReply> {
42
+ const joined = invocation.messages.map((m) => m.text).join('\n');
43
+ return { text: `[${prefix}] ${joined}` };
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Shell runtime: spawns the given command, pipes invocation JSON to stdin,
50
+ * captures stdout, returns it as the agent reply. The command is interpreted
51
+ * via /bin/sh -c so the caller can use pipelines / args freely (M3 keeps it
52
+ * simple; a future M4 can add per-arg parsing or env injection).
53
+ */
54
+ export interface ShellRuntimeOptions {
55
+ command: string;
56
+ cwd?: string;
57
+ timeoutMs?: number;
58
+ env?: Record<string, string>;
59
+ }
60
+
61
+ export function makeShellRuntime(options: ShellRuntimeOptions): AgentRuntime {
62
+ const cmd = options.command.trim();
63
+ if (!cmd) throw new Error('shell runtime requires a non-empty command');
64
+ return {
65
+ name: `shell:${cmd}`,
66
+ async run(invocation): Promise<AgentReply> {
67
+ return await new Promise<AgentReply>((resolve, reject) => {
68
+ const child = spawn('/bin/sh', ['-c', cmd], {
69
+ cwd: options.cwd,
70
+ env: { ...process.env, ...(options.env ?? {}) },
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ });
73
+ const stdoutChunks: Buffer[] = [];
74
+ const stderrChunks: Buffer[] = [];
75
+ let settled = false;
76
+ const timer = options.timeoutMs
77
+ ? setTimeout(() => {
78
+ if (settled) return;
79
+ settled = true;
80
+ child.kill('SIGTERM');
81
+ reject(new Error(`shell runtime timeout after ${options.timeoutMs}ms`));
82
+ }, options.timeoutMs)
83
+ : null;
84
+ const onAbort = (): void => {
85
+ if (settled) return;
86
+ settled = true;
87
+ if (timer) clearTimeout(timer);
88
+ try {
89
+ child.kill('SIGTERM');
90
+ } catch {
91
+ /* ignore */
92
+ }
93
+ reject(new Error('aborted'));
94
+ };
95
+ if (invocation.signal) {
96
+ if (invocation.signal.aborted) {
97
+ onAbort();
98
+ return;
99
+ }
100
+ invocation.signal.addEventListener('abort', onAbort, { once: true });
101
+ }
102
+ child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
103
+ child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
104
+ child.on('error', (err) => {
105
+ if (settled) return;
106
+ settled = true;
107
+ if (timer) clearTimeout(timer);
108
+ reject(err);
109
+ });
110
+ child.on('close', (code) => {
111
+ if (settled) return;
112
+ settled = true;
113
+ if (timer) clearTimeout(timer);
114
+ if (code !== 0) {
115
+ const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
116
+ reject(new Error(`shell runtime exited ${code}${stderr ? `: ${stderr}` : ''}`));
117
+ return;
118
+ }
119
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8').trim();
120
+ resolve({ text: stdout, meta: { exitCode: code } });
121
+ });
122
+ try {
123
+ child.stdin.end(JSON.stringify(invocation));
124
+ } catch (err) {
125
+ if (settled) return;
126
+ settled = true;
127
+ if (timer) clearTimeout(timer);
128
+ reject(err);
129
+ }
130
+ });
131
+ },
132
+ };
133
+ }
134
+
135
+ /** Convenience parser: "echo", "echo:bot", "shell:bun -e 'console.log(\"hi\")'". */
136
+ export function parseRuntimeSpec(spec: string): AgentRuntime {
137
+ const trimmed = spec.trim();
138
+ if (!trimmed || trimmed === 'echo') return makeEchoRuntime();
139
+ if (trimmed.startsWith('echo:')) return makeEchoRuntime(trimmed.slice('echo:'.length));
140
+ if (trimmed.startsWith('shell:')) return makeShellRuntime({ command: trimmed.slice('shell:'.length) });
141
+ throw new Error(`unknown agent runtime spec: ${spec}`);
142
+ }