@rozek/nanoclaw 0.0.1
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/.claude/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +325 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +774 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/cwd/SKILL.md +32 -0
- package/container/skills/pwd/SKILL.md +19 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +10 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1843 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +511 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +40 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +276 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +104 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +194 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +241 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +15 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1931 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +768 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +379 -0
- package/src/index.ts +832 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +328 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +109 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { DATA_DIR, MAX_CONCURRENT_CONTAINERS } from './config.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
interface QueuedTask {
|
|
9
|
+
id: string;
|
|
10
|
+
groupJid: string;
|
|
11
|
+
fn: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_RETRIES = 5;
|
|
15
|
+
const BASE_RETRY_MS = 5000;
|
|
16
|
+
|
|
17
|
+
interface GroupState {
|
|
18
|
+
active: boolean;
|
|
19
|
+
idleWaiting: boolean;
|
|
20
|
+
isTaskContainer: boolean;
|
|
21
|
+
runningTaskId: string | null;
|
|
22
|
+
pendingMessages: boolean;
|
|
23
|
+
pendingTasks: QueuedTask[];
|
|
24
|
+
process: ChildProcess | null;
|
|
25
|
+
containerName: string | null;
|
|
26
|
+
groupFolder: string | null;
|
|
27
|
+
retryCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class GroupQueue {
|
|
31
|
+
private groups = new Map<string, GroupState>();
|
|
32
|
+
private activeCount = 0;
|
|
33
|
+
private waitingGroups: string[] = [];
|
|
34
|
+
private processMessagesFn: ((groupJid: string) => Promise<boolean>) | null =
|
|
35
|
+
null;
|
|
36
|
+
private shuttingDown = false;
|
|
37
|
+
|
|
38
|
+
private getGroup(groupJid: string): GroupState {
|
|
39
|
+
let state = this.groups.get(groupJid);
|
|
40
|
+
if (!state) {
|
|
41
|
+
state = {
|
|
42
|
+
active: false,
|
|
43
|
+
idleWaiting: false,
|
|
44
|
+
isTaskContainer: false,
|
|
45
|
+
runningTaskId: null,
|
|
46
|
+
pendingMessages: false,
|
|
47
|
+
pendingTasks: [],
|
|
48
|
+
process: null,
|
|
49
|
+
containerName: null,
|
|
50
|
+
groupFolder: null,
|
|
51
|
+
retryCount: 0,
|
|
52
|
+
};
|
|
53
|
+
this.groups.set(groupJid, state);
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setProcessMessagesFn(fn: (groupJid: string) => Promise<boolean>): void {
|
|
59
|
+
this.processMessagesFn = fn;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
enqueueMessageCheck(groupJid: string): void {
|
|
63
|
+
if (this.shuttingDown) return;
|
|
64
|
+
|
|
65
|
+
const state = this.getGroup(groupJid);
|
|
66
|
+
|
|
67
|
+
if (state.active) {
|
|
68
|
+
state.pendingMessages = true;
|
|
69
|
+
logger.debug({ groupJid }, 'Container active, message queued');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
|
|
74
|
+
state.pendingMessages = true;
|
|
75
|
+
if (!this.waitingGroups.includes(groupJid)) {
|
|
76
|
+
this.waitingGroups.push(groupJid);
|
|
77
|
+
}
|
|
78
|
+
logger.debug(
|
|
79
|
+
{ groupJid, activeCount: this.activeCount },
|
|
80
|
+
'At concurrency limit, message queued',
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.runForGroup(groupJid, 'messages').catch((err) =>
|
|
86
|
+
logger.error({ groupJid, err }, 'Unhandled error in runForGroup'),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
enqueueTask(groupJid: string, taskId: string, fn: () => Promise<void>): void {
|
|
91
|
+
if (this.shuttingDown) return;
|
|
92
|
+
|
|
93
|
+
const state = this.getGroup(groupJid);
|
|
94
|
+
|
|
95
|
+
// Prevent double-queuing: check both pending and currently-running task
|
|
96
|
+
if (state.runningTaskId === taskId) {
|
|
97
|
+
logger.debug({ groupJid, taskId }, 'Task already running, skipping');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (state.pendingTasks.some((t) => t.id === taskId)) {
|
|
101
|
+
logger.debug({ groupJid, taskId }, 'Task already queued, skipping');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (state.active) {
|
|
106
|
+
state.pendingTasks.push({ id: taskId, groupJid, fn });
|
|
107
|
+
if (state.idleWaiting) {
|
|
108
|
+
this.closeStdin(groupJid);
|
|
109
|
+
}
|
|
110
|
+
logger.debug({ groupJid, taskId }, 'Container active, task queued');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
|
|
115
|
+
state.pendingTasks.push({ id: taskId, groupJid, fn });
|
|
116
|
+
if (!this.waitingGroups.includes(groupJid)) {
|
|
117
|
+
this.waitingGroups.push(groupJid);
|
|
118
|
+
}
|
|
119
|
+
logger.debug(
|
|
120
|
+
{ groupJid, taskId, activeCount: this.activeCount },
|
|
121
|
+
'At concurrency limit, task queued',
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Run immediately
|
|
127
|
+
this.runTask(groupJid, { id: taskId, groupJid, fn }).catch((err) =>
|
|
128
|
+
logger.error({ groupJid, taskId, err }, 'Unhandled error in runTask'),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
registerProcess(
|
|
133
|
+
groupJid: string,
|
|
134
|
+
proc: ChildProcess,
|
|
135
|
+
containerName: string,
|
|
136
|
+
groupFolder?: string,
|
|
137
|
+
): void {
|
|
138
|
+
const state = this.getGroup(groupJid);
|
|
139
|
+
state.process = proc;
|
|
140
|
+
state.containerName = containerName;
|
|
141
|
+
if (groupFolder) state.groupFolder = groupFolder;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mark the container as idle-waiting (finished work, waiting for IPC input).
|
|
146
|
+
* If tasks are pending, preempt the idle container immediately.
|
|
147
|
+
*/
|
|
148
|
+
notifyIdle(groupJid: string): void {
|
|
149
|
+
const state = this.getGroup(groupJid);
|
|
150
|
+
state.idleWaiting = true;
|
|
151
|
+
if (state.pendingTasks.length > 0) {
|
|
152
|
+
this.closeStdin(groupJid);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Send a follow-up message to the active container via IPC file.
|
|
158
|
+
* Returns true if the message was written, false if no active container.
|
|
159
|
+
*/
|
|
160
|
+
sendMessage(groupJid: string, text: string): boolean {
|
|
161
|
+
const state = this.getGroup(groupJid);
|
|
162
|
+
if (!state.active || !state.groupFolder || state.isTaskContainer)
|
|
163
|
+
return false;
|
|
164
|
+
state.idleWaiting = false; // Agent is about to receive work, no longer idle
|
|
165
|
+
|
|
166
|
+
const inputDir = path.join(DATA_DIR, 'ipc', state.groupFolder, 'input');
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(inputDir, { recursive: true });
|
|
169
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}.json`;
|
|
170
|
+
const filepath = path.join(inputDir, filename);
|
|
171
|
+
const tempPath = `${filepath}.tmp`;
|
|
172
|
+
fs.writeFileSync(tempPath, JSON.stringify({ type: 'message', text }));
|
|
173
|
+
fs.renameSync(tempPath, filepath);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Signal the active container to wind down by writing a close sentinel.
|
|
182
|
+
*/
|
|
183
|
+
closeStdin(groupJid: string): void {
|
|
184
|
+
const state = this.getGroup(groupJid);
|
|
185
|
+
if (!state.active || !state.groupFolder) return;
|
|
186
|
+
|
|
187
|
+
const inputDir = path.join(DATA_DIR, 'ipc', state.groupFolder, 'input');
|
|
188
|
+
try {
|
|
189
|
+
fs.mkdirSync(inputDir, { recursive: true });
|
|
190
|
+
fs.writeFileSync(path.join(inputDir, '_close'), '');
|
|
191
|
+
} catch {
|
|
192
|
+
// ignore
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Forcefully cancel the active container for a group.
|
|
198
|
+
* Writes the close sentinel (graceful) and immediately kills the spawned
|
|
199
|
+
* container process so the current SDK query is aborted right away.
|
|
200
|
+
*/
|
|
201
|
+
cancelContainer(groupJid: string): void {
|
|
202
|
+
this.closeStdin(groupJid); // graceful signal first
|
|
203
|
+
const state = this.getGroup(groupJid);
|
|
204
|
+
if (state.process && !state.process.killed) {
|
|
205
|
+
logger.info({ groupJid }, 'Killing container process for cancel request');
|
|
206
|
+
state.process.kill('SIGTERM');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async runForGroup(
|
|
211
|
+
groupJid: string,
|
|
212
|
+
reason: 'messages' | 'drain',
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
const state = this.getGroup(groupJid);
|
|
215
|
+
state.active = true;
|
|
216
|
+
state.idleWaiting = false;
|
|
217
|
+
state.isTaskContainer = false;
|
|
218
|
+
state.pendingMessages = false;
|
|
219
|
+
this.activeCount++;
|
|
220
|
+
|
|
221
|
+
logger.debug(
|
|
222
|
+
{ groupJid, reason, activeCount: this.activeCount },
|
|
223
|
+
'Starting container for group',
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (this.processMessagesFn) {
|
|
228
|
+
const success = await this.processMessagesFn(groupJid);
|
|
229
|
+
if (success) {
|
|
230
|
+
state.retryCount = 0;
|
|
231
|
+
} else {
|
|
232
|
+
this.scheduleRetry(groupJid, state);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error({ groupJid, err }, 'Error processing messages for group');
|
|
237
|
+
this.scheduleRetry(groupJid, state);
|
|
238
|
+
} finally {
|
|
239
|
+
state.active = false;
|
|
240
|
+
state.process = null;
|
|
241
|
+
state.containerName = null;
|
|
242
|
+
state.groupFolder = null;
|
|
243
|
+
this.activeCount--;
|
|
244
|
+
this.drainGroup(groupJid);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async runTask(groupJid: string, task: QueuedTask): Promise<void> {
|
|
249
|
+
const state = this.getGroup(groupJid);
|
|
250
|
+
state.active = true;
|
|
251
|
+
state.idleWaiting = false;
|
|
252
|
+
state.isTaskContainer = true;
|
|
253
|
+
state.runningTaskId = task.id;
|
|
254
|
+
this.activeCount++;
|
|
255
|
+
|
|
256
|
+
logger.debug(
|
|
257
|
+
{ groupJid, taskId: task.id, activeCount: this.activeCount },
|
|
258
|
+
'Running queued task',
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await task.fn();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
logger.error({ groupJid, taskId: task.id, err }, 'Error running task');
|
|
265
|
+
} finally {
|
|
266
|
+
state.active = false;
|
|
267
|
+
state.isTaskContainer = false;
|
|
268
|
+
state.runningTaskId = null;
|
|
269
|
+
state.process = null;
|
|
270
|
+
state.containerName = null;
|
|
271
|
+
state.groupFolder = null;
|
|
272
|
+
this.activeCount--;
|
|
273
|
+
this.drainGroup(groupJid);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private scheduleRetry(groupJid: string, state: GroupState): void {
|
|
278
|
+
state.retryCount++;
|
|
279
|
+
if (state.retryCount > MAX_RETRIES) {
|
|
280
|
+
logger.error(
|
|
281
|
+
{ groupJid, retryCount: state.retryCount },
|
|
282
|
+
'Max retries exceeded, dropping messages (will retry on next incoming message)',
|
|
283
|
+
);
|
|
284
|
+
state.retryCount = 0;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const delayMs = BASE_RETRY_MS * Math.pow(2, state.retryCount - 1);
|
|
289
|
+
logger.info(
|
|
290
|
+
{ groupJid, retryCount: state.retryCount, delayMs },
|
|
291
|
+
'Scheduling retry with backoff',
|
|
292
|
+
);
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
if (!this.shuttingDown) {
|
|
295
|
+
this.enqueueMessageCheck(groupJid);
|
|
296
|
+
}
|
|
297
|
+
}, delayMs);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private drainGroup(groupJid: string): void {
|
|
301
|
+
if (this.shuttingDown) return;
|
|
302
|
+
|
|
303
|
+
const state = this.getGroup(groupJid);
|
|
304
|
+
|
|
305
|
+
// Tasks first (they won't be re-discovered from SQLite like messages)
|
|
306
|
+
if (state.pendingTasks.length > 0) {
|
|
307
|
+
const task = state.pendingTasks.shift()!;
|
|
308
|
+
this.runTask(groupJid, task).catch((err) =>
|
|
309
|
+
logger.error(
|
|
310
|
+
{ groupJid, taskId: task.id, err },
|
|
311
|
+
'Unhandled error in runTask (drain)',
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Then pending messages
|
|
318
|
+
if (state.pendingMessages) {
|
|
319
|
+
this.runForGroup(groupJid, 'drain').catch((err) =>
|
|
320
|
+
logger.error(
|
|
321
|
+
{ groupJid, err },
|
|
322
|
+
'Unhandled error in runForGroup (drain)',
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Nothing pending for this group; check if other groups are waiting for a slot
|
|
329
|
+
this.drainWaiting();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private drainWaiting(): void {
|
|
333
|
+
while (
|
|
334
|
+
this.waitingGroups.length > 0 &&
|
|
335
|
+
this.activeCount < MAX_CONCURRENT_CONTAINERS
|
|
336
|
+
) {
|
|
337
|
+
const nextJid = this.waitingGroups.shift()!;
|
|
338
|
+
const state = this.getGroup(nextJid);
|
|
339
|
+
|
|
340
|
+
// Prioritize tasks over messages
|
|
341
|
+
if (state.pendingTasks.length > 0) {
|
|
342
|
+
const task = state.pendingTasks.shift()!;
|
|
343
|
+
this.runTask(nextJid, task).catch((err) =>
|
|
344
|
+
logger.error(
|
|
345
|
+
{ groupJid: nextJid, taskId: task.id, err },
|
|
346
|
+
'Unhandled error in runTask (waiting)',
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
} else if (state.pendingMessages) {
|
|
350
|
+
this.runForGroup(nextJid, 'drain').catch((err) =>
|
|
351
|
+
logger.error(
|
|
352
|
+
{ groupJid: nextJid, err },
|
|
353
|
+
'Unhandled error in runForGroup (waiting)',
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
// If neither pending, skip this group
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async shutdown(_gracePeriodMs: number): Promise<void> {
|
|
362
|
+
this.shuttingDown = true;
|
|
363
|
+
|
|
364
|
+
// Count active containers but don't kill them — they'll finish on their own
|
|
365
|
+
// via idle timeout or container timeout. The --rm flag cleans them up on exit.
|
|
366
|
+
// This prevents WhatsApp reconnection restarts from killing working agents.
|
|
367
|
+
const activeContainers: string[] = [];
|
|
368
|
+
for (const [jid, state] of this.groups) {
|
|
369
|
+
if (state.process && !state.process.killed && state.containerName) {
|
|
370
|
+
activeContainers.push(state.containerName);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
logger.info(
|
|
375
|
+
{ activeCount: this.activeCount, detachedContainers: activeContainers },
|
|
376
|
+
'GroupQueue shutting down (containers detached, not killed)',
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|