@bbigbang/runtime-acp 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.
@@ -0,0 +1,7 @@
1
+ import type { Db } from '../db/db.js';
2
+ export declare function buildReplayContextFromRecentRuns(db: Db, params: {
3
+ sessionKey: string;
4
+ excludeRunId: string;
5
+ maxRuns: number;
6
+ maxChars: number;
7
+ }): string;
@@ -0,0 +1,146 @@
1
+ export function buildReplayContextFromRecentRuns(db, params) {
2
+ const runs = db
3
+ .prepare(`
4
+ SELECT run_id as runId,
5
+ prompt_text as promptText,
6
+ stop_reason as stopReason,
7
+ error,
8
+ started_at as startedAt
9
+ FROM runs
10
+ WHERE session_key = ? AND run_id != ?
11
+ ORDER BY started_at DESC
12
+ LIMIT ?
13
+ `)
14
+ .all(params.sessionKey, params.excludeRunId, params.maxRuns);
15
+ const chronological = runs.slice().reverse();
16
+ const assistantLabel = getSessionAssistantLabel(db, params.sessionKey) ?? 'Assistant';
17
+ const blocks = [];
18
+ for (const run of chronological) {
19
+ const rows = db
20
+ .prepare('SELECT payload_json as payloadJson FROM events WHERE run_id = ? AND method = ? ORDER BY seq ASC')
21
+ .all(run.runId, 'session/update');
22
+ let assistantText = '';
23
+ for (const row of rows) {
24
+ try {
25
+ const payload = JSON.parse(row.payloadJson);
26
+ const update = payload?.update;
27
+ if (update?.sessionUpdate !== 'agent_message_chunk')
28
+ continue;
29
+ assistantText += update?.content?.text ?? '';
30
+ }
31
+ catch {
32
+ // ignore malformed rows
33
+ }
34
+ }
35
+ const assistantLine = assistantText.trim()
36
+ ? assistantText.trim()
37
+ : run.error
38
+ ? `[error] ${run.error}`
39
+ : run.stopReason && run.stopReason !== 'handoff_bootstrap'
40
+ ? `[stop_reason] ${run.stopReason}`
41
+ : '';
42
+ const normalizedUserText = normalizeReplayUserText(run.promptText);
43
+ if (normalizedUserText) {
44
+ blocks.push(`User: ${normalizedUserText}`);
45
+ }
46
+ if (assistantLine)
47
+ blocks.push(`${assistantLabel}: ${assistantLine}`);
48
+ }
49
+ const raw = blocks.join('\n');
50
+ if (!raw.trim())
51
+ return '';
52
+ const header = 'Context (previous messages, for continuity after restart/GC):\n';
53
+ const full = header + raw;
54
+ if (full.length <= params.maxChars)
55
+ return full;
56
+ return header + raw.slice(Math.max(0, raw.length - params.maxChars));
57
+ }
58
+ function stripReplyContract(promptText) {
59
+ if (!promptText.startsWith('[Reply contract]'))
60
+ return promptText;
61
+ const splitIndex = promptText.indexOf('\n\n');
62
+ return splitIndex >= 0 ? promptText.slice(splitIndex + 2) : promptText;
63
+ }
64
+ function normalizeComparableReplayText(value) {
65
+ return (value ?? '').replace(/\s+/g, ' ').trim();
66
+ }
67
+ function extractPromptSection(promptText, marker, stopMarkers) {
68
+ const start = promptText.indexOf(marker);
69
+ if (start < 0)
70
+ return null;
71
+ const remainder = promptText.slice(start + marker.length);
72
+ let end = remainder.length;
73
+ for (const stopMarker of stopMarkers) {
74
+ const idx = remainder.indexOf(stopMarker);
75
+ if (idx >= 0 && idx < end)
76
+ end = idx;
77
+ }
78
+ const body = remainder.slice(0, end).trim();
79
+ return body || null;
80
+ }
81
+ function extractTaskAttachmentSection(promptText) {
82
+ const start = promptText.indexOf('\n\n[Task attachment');
83
+ if (start < 0)
84
+ return null;
85
+ const remainder = promptText.slice(start + 2);
86
+ let end = remainder.length;
87
+ for (const stopMarker of ['\n\n[Triggered message metadata]\n', '\n\nRules:\n']) {
88
+ const idx = remainder.indexOf(stopMarker);
89
+ if (idx >= 0 && idx < end)
90
+ end = idx;
91
+ }
92
+ const section = remainder.slice(0, end).trim();
93
+ return section || null;
94
+ }
95
+ function extractTriggeredMessageBody(promptText) {
96
+ const marker = '[Triggered message body]\n';
97
+ const start = promptText.indexOf(marker);
98
+ if (start < 0)
99
+ return null;
100
+ const remainder = promptText.slice(start + marker.length);
101
+ // DM task handoff prompts append a Rules block after the original trigger text.
102
+ // Keep ordinary user messages intact even if they literally contain "Rules:".
103
+ const rulesIndex = promptText.includes('[DM Task Thread Handoff]')
104
+ ? remainder.lastIndexOf('\n\nRules:\n')
105
+ : -1;
106
+ const body = (rulesIndex >= 0 ? remainder.slice(0, rulesIndex) : remainder).trim();
107
+ return body || null;
108
+ }
109
+ function buildTaskHandoffReplayUserText(promptText) {
110
+ if (!promptText.includes('[DM Task Thread Handoff]') && !promptText.includes('[Channel Task Thread Handoff]')) {
111
+ return null;
112
+ }
113
+ const triggerBody = extractTriggeredMessageBody(promptText);
114
+ const taskBrief = extractPromptSection(promptText, 'Task brief / goal / done criteria:\n', ['\n\n[Task attachment', '\n\n[Triggered message metadata]\n', '\n\nRules:\n']);
115
+ const attachmentSection = extractTaskAttachmentSection(promptText);
116
+ if (!taskBrief && !attachmentSection)
117
+ return triggerBody;
118
+ const sameBriefAsTrigger = taskBrief
119
+ && triggerBody
120
+ && normalizeComparableReplayText(taskBrief) === normalizeComparableReplayText(triggerBody);
121
+ if (sameBriefAsTrigger) {
122
+ return attachmentSection
123
+ ? [triggerBody, attachmentSection].filter(Boolean).join('\n\n')
124
+ : triggerBody;
125
+ }
126
+ const parts = [];
127
+ if (taskBrief)
128
+ parts.push(`Task brief / goal / done criteria:\n${taskBrief}`);
129
+ if (attachmentSection)
130
+ parts.push(attachmentSection);
131
+ if (triggerBody)
132
+ parts.push(`Original trigger:\n${triggerBody}`);
133
+ return parts.length > 0 ? parts.join('\n\n') : null;
134
+ }
135
+ function normalizeReplayUserText(promptText) {
136
+ const stripped = stripReplyContract(promptText).trim();
137
+ return buildTaskHandoffReplayUserText(stripped) ?? extractTriggeredMessageBody(stripped) ?? stripped;
138
+ }
139
+ function getSessionAssistantLabel(db, sessionKey) {
140
+ const row = db.prepare('SELECT system_prompt_text as systemPromptText FROM sessions WHERE session_key = ?').get(sessionKey);
141
+ const promptText = row?.systemPromptText?.trim();
142
+ if (!promptText)
143
+ return null;
144
+ const match = /You are "([^"]+)"/.exec(promptText);
145
+ return match?.[1]?.trim() || null;
146
+ }
@@ -0,0 +1,79 @@
1
+ import type { Db } from '../db/db.js';
2
+ export type Platform = 'discord' | 'telegram' | 'feishu' | 'web' | 'node';
3
+ export type StoredClaudeSessionModeId = 'default' | 'acceptEdits' | 'plan' | 'bypassPermissions';
4
+ export declare const SHARED_CHAT_SCOPE_USER_ID = "__chat_scope__";
5
+ export type ConversationKey = {
6
+ platform: Platform;
7
+ chatId: string;
8
+ threadId: string | null;
9
+ userId: string;
10
+ scopeUserId?: string | null;
11
+ };
12
+ export type SessionBinding = {
13
+ bindingKey: string;
14
+ sessionKey: string;
15
+ };
16
+ export declare function bindingKeyFromConversationKey(key: ConversationKey): string;
17
+ export declare function bindingScopeUserId(key: ConversationKey): string;
18
+ export declare function getBinding(db: Db, key: ConversationKey): SessionBinding | null;
19
+ export declare function upsertBinding(db: Db, key: ConversationKey, sessionKey: string): SessionBinding;
20
+ export declare function deleteBinding(db: Db, key: ConversationKey): void;
21
+ export declare function createSession(db: Db, params: {
22
+ sessionKey: string;
23
+ agentCommand: string;
24
+ agentArgs: string[];
25
+ cwd: string;
26
+ loadSupported: boolean;
27
+ claudeModeId?: StoredClaudeSessionModeId | null;
28
+ claudeModelId?: string | null;
29
+ }): void;
30
+ export declare function updateAcpSessionId(db: Db, sessionKey: string, acpSessionId: string): void;
31
+ export declare function updateSessionRuntimeState(db: Db, params: {
32
+ sessionKey: string;
33
+ acpSessionId: string;
34
+ systemPromptText?: string | null;
35
+ }): void;
36
+ export declare function clearAcpSessionId(db: Db, sessionKey: string): void;
37
+ export declare function updateLoadSupported(db: Db, sessionKey: string, loadSupported: boolean): void;
38
+ export declare function updateSessionCwd(db: Db, sessionKey: string, cwd: string): void;
39
+ export declare function updateSessionAgentConfig(db: Db, params: {
40
+ sessionKey: string;
41
+ agentCommand: string;
42
+ agentArgs: string[];
43
+ }): void;
44
+ export declare function getSession(db: Db, sessionKey: string): {
45
+ sessionKey: string;
46
+ agentCommand: string;
47
+ agentArgsJson: string;
48
+ acpSessionId: string | null;
49
+ systemPromptText: string | null;
50
+ cwd: string;
51
+ loadSupported: number;
52
+ claudeModeId: StoredClaudeSessionModeId | null;
53
+ claudeModelId: string | null;
54
+ } | null;
55
+ export declare function updateClaudeSessionControls(db: Db, params: {
56
+ sessionKey: string;
57
+ claudeModeId?: StoredClaudeSessionModeId | null;
58
+ claudeModelId?: string | null;
59
+ }): void;
60
+ export declare function insertClaudeSessionUserMessage(db: Db, params: {
61
+ sessionKey: string;
62
+ runId?: string | null;
63
+ messageUuid: string;
64
+ }): void;
65
+ export declare function getLatestClaudeSessionUserMessage(db: Db, sessionKey: string): {
66
+ messageUuid: string;
67
+ runId: string | null;
68
+ createdAt: number;
69
+ } | null;
70
+ export declare function createRun(db: Db, params: {
71
+ runId: string;
72
+ sessionKey: string;
73
+ promptText: string;
74
+ }): void;
75
+ export declare function finishRun(db: Db, params: {
76
+ runId: string;
77
+ stopReason?: string;
78
+ error?: string;
79
+ }): void;
@@ -0,0 +1,126 @@
1
+ // Shared scope marker for group/channel conversations.
2
+ export const SHARED_CHAT_SCOPE_USER_ID = '__chat_scope__';
3
+ export function bindingKeyFromConversationKey(key) {
4
+ return [
5
+ key.platform,
6
+ key.chatId,
7
+ key.threadId ?? '-',
8
+ bindingScopeUserId(key),
9
+ ].join(':');
10
+ }
11
+ export function bindingScopeUserId(key) {
12
+ return key.scopeUserId?.trim() ? key.scopeUserId : key.userId;
13
+ }
14
+ export function getBinding(db, key) {
15
+ const bindingKey = bindingKeyFromConversationKey(key);
16
+ const row = db
17
+ .prepare('SELECT binding_key as bindingKey, session_key as sessionKey FROM bindings WHERE binding_key = ?')
18
+ .get(bindingKey);
19
+ return row ?? null;
20
+ }
21
+ export function upsertBinding(db, key, sessionKey) {
22
+ const bindingKey = bindingKeyFromConversationKey(key);
23
+ const now = Date.now();
24
+ db.prepare(`
25
+ INSERT INTO bindings(binding_key, platform, chat_id, thread_id, user_id, session_key, created_at, updated_at)
26
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?)
27
+ ON CONFLICT(binding_key) DO UPDATE SET
28
+ session_key = excluded.session_key,
29
+ updated_at = excluded.updated_at
30
+ `).run(bindingKey, key.platform, key.chatId, key.threadId, bindingScopeUserId(key), sessionKey, now, now);
31
+ return { bindingKey, sessionKey };
32
+ }
33
+ export function deleteBinding(db, key) {
34
+ const bindingKey = bindingKeyFromConversationKey(key);
35
+ // Bindings are referenced by several tables; delete dependents first.
36
+ db.prepare('DELETE FROM jobs WHERE binding_key = ?').run(bindingKey);
37
+ db.prepare('DELETE FROM tool_policies WHERE binding_key = ?').run(bindingKey);
38
+ db.prepare('DELETE FROM tool_allow_prefixes WHERE binding_key = ?').run(bindingKey);
39
+ db.prepare('DELETE FROM ui_prefs WHERE binding_key = ?').run(bindingKey);
40
+ db.prepare('DELETE FROM delivery_checkpoints WHERE binding_key = ?').run(bindingKey);
41
+ db.prepare('DELETE FROM bindings WHERE binding_key = ?').run(bindingKey);
42
+ }
43
+ export function createSession(db, params) {
44
+ const now = Date.now();
45
+ db.prepare(`
46
+ INSERT INTO sessions(session_key, agent_command, agent_args_json, acp_session_id, load_supported, cwd, claude_mode_id, claude_model_id, created_at, updated_at)
47
+ VALUES(?, ?, ?, NULL, ?, ?, ?, ?, ?, ?)
48
+ `).run(params.sessionKey, params.agentCommand, JSON.stringify(params.agentArgs), params.loadSupported ? 1 : 0, params.cwd, params.claudeModeId ?? null, params.claudeModelId ?? null, now, now);
49
+ }
50
+ export function updateAcpSessionId(db, sessionKey, acpSessionId) {
51
+ const now = Date.now();
52
+ db.prepare('UPDATE sessions SET acp_session_id = ?, updated_at = ? WHERE session_key = ?').run(acpSessionId, now, sessionKey);
53
+ }
54
+ export function updateSessionRuntimeState(db, params) {
55
+ const now = Date.now();
56
+ db.prepare('UPDATE sessions SET acp_session_id = ?, system_prompt_text = ?, updated_at = ? WHERE session_key = ?').run(params.acpSessionId, params.systemPromptText ?? null, now, params.sessionKey);
57
+ }
58
+ export function clearAcpSessionId(db, sessionKey) {
59
+ const now = Date.now();
60
+ db.prepare('UPDATE sessions SET acp_session_id = NULL, system_prompt_text = NULL, updated_at = ? WHERE session_key = ?').run(now, sessionKey);
61
+ db.prepare('DELETE FROM claude_session_user_messages WHERE session_key = ?').run(sessionKey);
62
+ }
63
+ export function updateLoadSupported(db, sessionKey, loadSupported) {
64
+ const now = Date.now();
65
+ db.prepare('UPDATE sessions SET load_supported = ?, updated_at = ? WHERE session_key = ?').run(loadSupported ? 1 : 0, now, sessionKey);
66
+ }
67
+ export function updateSessionCwd(db, sessionKey, cwd) {
68
+ const now = Date.now();
69
+ db.prepare('UPDATE sessions SET cwd = ?, updated_at = ? WHERE session_key = ?').run(cwd, now, sessionKey);
70
+ }
71
+ export function updateSessionAgentConfig(db, params) {
72
+ const now = Date.now();
73
+ db.prepare(`
74
+ UPDATE sessions
75
+ SET agent_command = ?,
76
+ agent_args_json = ?,
77
+ acp_session_id = NULL,
78
+ system_prompt_text = NULL,
79
+ updated_at = ?
80
+ WHERE session_key = ?
81
+ `).run(params.agentCommand, JSON.stringify(params.agentArgs), now, params.sessionKey);
82
+ }
83
+ export function getSession(db, sessionKey) {
84
+ const row = db
85
+ .prepare(`SELECT session_key as sessionKey,
86
+ agent_command as agentCommand,
87
+ agent_args_json as agentArgsJson,
88
+ acp_session_id as acpSessionId,
89
+ system_prompt_text as systemPromptText,
90
+ cwd,
91
+ load_supported as loadSupported,
92
+ claude_mode_id as claudeModeId,
93
+ claude_model_id as claudeModelId
94
+ FROM sessions
95
+ WHERE session_key = ?`)
96
+ .get(sessionKey);
97
+ return row ?? null;
98
+ }
99
+ export function updateClaudeSessionControls(db, params) {
100
+ const now = Date.now();
101
+ db.prepare(`UPDATE sessions
102
+ SET claude_mode_id = ?,
103
+ claude_model_id = ?,
104
+ updated_at = ?
105
+ WHERE session_key = ?`).run(params.claudeModeId ?? null, params.claudeModelId ?? null, now, params.sessionKey);
106
+ }
107
+ export function insertClaudeSessionUserMessage(db, params) {
108
+ db.prepare(`INSERT OR REPLACE INTO claude_session_user_messages(session_key, run_id, message_uuid, created_at)
109
+ VALUES(?, ?, ?, ?)`).run(params.sessionKey, params.runId ?? null, params.messageUuid, Date.now());
110
+ }
111
+ export function getLatestClaudeSessionUserMessage(db, sessionKey) {
112
+ const row = db.prepare(`SELECT message_uuid as messageUuid,
113
+ run_id as runId,
114
+ created_at as createdAt
115
+ FROM claude_session_user_messages
116
+ WHERE session_key = ?
117
+ ORDER BY created_at DESC, rowid DESC
118
+ LIMIT 1`).get(sessionKey);
119
+ return row ?? null;
120
+ }
121
+ export function createRun(db, params) {
122
+ db.prepare('INSERT INTO runs(run_id, session_key, prompt_text, started_at) VALUES(?, ?, ?, ?)').run(params.runId, params.sessionKey, params.promptText, Date.now());
123
+ }
124
+ export function finishRun(db, params) {
125
+ db.prepare('UPDATE runs SET ended_at = ?, stop_reason = ?, error = ? WHERE run_id = ?').run(Date.now(), params.stopReason ?? null, params.error ?? null, params.runId);
126
+ }
@@ -0,0 +1,36 @@
1
+ import type { Db } from '../db/db.js';
2
+ export type ToolKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
3
+ export type PersistentToolPolicy = 'allow' | 'reject';
4
+ export declare const TOOL_KINDS: ToolKind[];
5
+ export declare function parseToolKind(value: unknown): ToolKind | null;
6
+ export type ToolMatchContext = {
7
+ method?: string;
8
+ params?: unknown;
9
+ toolCall?: unknown;
10
+ workspaceRoot?: string;
11
+ };
12
+ export type ToolAllowPrefixRule = {
13
+ toolKind: ToolKind;
14
+ argPrefix: string;
15
+ };
16
+ export declare class ToolAuth {
17
+ private readonly db;
18
+ private readonly onceGrants;
19
+ constructor(db: Db);
20
+ grantOnce(sessionKey: string, toolKind: ToolKind, count?: number): void;
21
+ setPersistentPolicy(bindingKey: string, toolKind: ToolKind, policy: PersistentToolPolicy): void;
22
+ getPersistentPolicy(bindingKey: string, toolKind: ToolKind): PersistentToolPolicy | null;
23
+ listPersistentPolicies(bindingKey: string, policy?: PersistentToolPolicy): Array<{
24
+ toolKind: ToolKind;
25
+ policy: PersistentToolPolicy;
26
+ }>;
27
+ clearPersistentPolicy(bindingKey: string, toolKind: ToolKind, policy?: PersistentToolPolicy): boolean;
28
+ clearPersistentPolicies(bindingKey: string, policy?: PersistentToolPolicy): number;
29
+ setAllowPrefixRule(bindingKey: string, toolKind: ToolKind, argPrefix: string): void;
30
+ listAllowPrefixRules(bindingKey: string, toolKind?: ToolKind): ToolAllowPrefixRule[];
31
+ clearAllowPrefixRule(bindingKey: string, toolKind: ToolKind, argPrefix: string): boolean;
32
+ clearAllowPrefixRules(bindingKey: string, toolKind?: ToolKind): number;
33
+ evaluatePersistentPolicy(bindingKey: string, toolKind: ToolKind, context?: ToolMatchContext): PersistentToolPolicy | null;
34
+ consume(sessionKey: string, toolKind: ToolKind, context?: ToolMatchContext): boolean;
35
+ private matchesAllowPrefixRule;
36
+ }