@ouro.bot/cli 0.1.0-alpha.347 → 0.1.0-alpha.349
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/changelog.json +13 -0
- package/dist/heart/core.js +116 -132
- package/dist/heart/daemon/cli-exec.js +408 -73
- package/dist/heart/daemon/cli-parse.js +90 -1
- package/dist/heart/provider-attempt.js +133 -0
- package/dist/heart/provider-credential-pool.js +3 -2
- package/dist/heart/provider-ping.js +116 -92
- package/dist/senses/trust-gate.js +5 -5
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PROVIDER_ATTEMPT_POLICY = exports.ProviderAttemptAbortError = void 0;
|
|
4
|
+
exports.runProviderAttempt = runProviderAttempt;
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
class ProviderAttemptAbortError extends Error {
|
|
7
|
+
constructor(message = "provider attempt aborted") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ProviderAttemptAbortError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ProviderAttemptAbortError = ProviderAttemptAbortError;
|
|
13
|
+
exports.DEFAULT_PROVIDER_ATTEMPT_POLICY = {
|
|
14
|
+
maxAttempts: 3,
|
|
15
|
+
baseDelayMs: 2_000,
|
|
16
|
+
backoffMultiplier: 2,
|
|
17
|
+
};
|
|
18
|
+
function sleep(delayMs) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
20
|
+
}
|
|
21
|
+
function normalizePolicy(policy) {
|
|
22
|
+
return {
|
|
23
|
+
...exports.DEFAULT_PROVIDER_ATTEMPT_POLICY,
|
|
24
|
+
...policy,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function toError(error) {
|
|
28
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
function classify(error, classifyError) {
|
|
31
|
+
if (!(error instanceof Error))
|
|
32
|
+
return "unknown";
|
|
33
|
+
try {
|
|
34
|
+
return classifyError(error);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function httpStatus(error) {
|
|
41
|
+
const status = error.status;
|
|
42
|
+
return typeof status === "number" ? status : null;
|
|
43
|
+
}
|
|
44
|
+
function delayForAttempt(policy, attempt) {
|
|
45
|
+
return policy.baseDelayMs * Math.pow(policy.backoffMultiplier, attempt - 1);
|
|
46
|
+
}
|
|
47
|
+
async function runProviderAttempt(input) {
|
|
48
|
+
const policy = normalizePolicy(input.policy);
|
|
49
|
+
const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
|
|
50
|
+
const wait = input.sleep ?? sleep;
|
|
51
|
+
const attempts = [];
|
|
52
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
53
|
+
try {
|
|
54
|
+
const value = await input.run();
|
|
55
|
+
attempts.push({
|
|
56
|
+
attempt,
|
|
57
|
+
provider: input.provider,
|
|
58
|
+
model: input.model,
|
|
59
|
+
operation: input.operation,
|
|
60
|
+
ok: true,
|
|
61
|
+
willRetry: false,
|
|
62
|
+
});
|
|
63
|
+
(0, runtime_1.emitNervesEvent)({
|
|
64
|
+
component: "engine",
|
|
65
|
+
event: "engine.provider_attempt_succeeded",
|
|
66
|
+
message: "provider attempt succeeded",
|
|
67
|
+
meta: { provider: input.provider, model: input.model, operation: input.operation, attempt, maxAttempts },
|
|
68
|
+
});
|
|
69
|
+
return { ok: true, value, attempts };
|
|
70
|
+
}
|
|
71
|
+
catch (caught) {
|
|
72
|
+
if (caught instanceof ProviderAttemptAbortError)
|
|
73
|
+
throw caught;
|
|
74
|
+
const error = toError(caught);
|
|
75
|
+
const classification = classify(caught, input.classifyError);
|
|
76
|
+
const willRetry = attempt < maxAttempts;
|
|
77
|
+
const delayMs = willRetry ? delayForAttempt(policy, attempt) : undefined;
|
|
78
|
+
const record = {
|
|
79
|
+
attempt,
|
|
80
|
+
provider: input.provider,
|
|
81
|
+
model: input.model,
|
|
82
|
+
operation: input.operation,
|
|
83
|
+
ok: false,
|
|
84
|
+
classification,
|
|
85
|
+
errorMessage: error.message,
|
|
86
|
+
httpStatus: httpStatus(error),
|
|
87
|
+
willRetry,
|
|
88
|
+
...(delayMs !== undefined ? { delayMs } : {}),
|
|
89
|
+
};
|
|
90
|
+
attempts.push(record);
|
|
91
|
+
if (!willRetry) {
|
|
92
|
+
(0, runtime_1.emitNervesEvent)({
|
|
93
|
+
level: "warn",
|
|
94
|
+
component: "engine",
|
|
95
|
+
event: "engine.provider_attempt_failed",
|
|
96
|
+
message: "provider attempt failed",
|
|
97
|
+
meta: {
|
|
98
|
+
provider: input.provider,
|
|
99
|
+
model: input.model,
|
|
100
|
+
operation: input.operation,
|
|
101
|
+
attempt,
|
|
102
|
+
maxAttempts,
|
|
103
|
+
classification,
|
|
104
|
+
errorMessage: error.message.slice(0, 200),
|
|
105
|
+
httpStatus: httpStatus(error),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return { ok: false, error, classification, attempts };
|
|
109
|
+
}
|
|
110
|
+
const retryDelayMs = delayMs;
|
|
111
|
+
(0, runtime_1.emitNervesEvent)({
|
|
112
|
+
component: "engine",
|
|
113
|
+
event: "engine.provider_attempt_retry",
|
|
114
|
+
message: "provider attempt failed; retrying",
|
|
115
|
+
meta: {
|
|
116
|
+
provider: input.provider,
|
|
117
|
+
model: input.model,
|
|
118
|
+
operation: input.operation,
|
|
119
|
+
attempt,
|
|
120
|
+
maxAttempts,
|
|
121
|
+
classification,
|
|
122
|
+
errorMessage: error.message.slice(0, 200),
|
|
123
|
+
httpStatus: httpStatus(error),
|
|
124
|
+
delayMs: retryDelayMs,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await input.onRetry?.(record, maxAttempts);
|
|
128
|
+
await wait(retryDelayMs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/* v8 ignore next 2 -- defensive: loop always returns on success or final failure @preserve */
|
|
132
|
+
return { ok: false, error: new Error("provider attempt loop ended unexpectedly"), classification: "unknown", attempts };
|
|
133
|
+
}
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.splitProviderCredentialFields = splitProviderCredentialFields;
|
|
36
37
|
exports.getProviderCredentialPoolPath = getProviderCredentialPoolPath;
|
|
37
38
|
exports.validateProviderCredentialPool = validateProviderCredentialPool;
|
|
38
39
|
exports.readProviderCredentialPool = readProviderCredentialPool;
|
|
@@ -105,7 +106,7 @@ function copyKnownFields(source, fields) {
|
|
|
105
106
|
}
|
|
106
107
|
return result;
|
|
107
108
|
}
|
|
108
|
-
function
|
|
109
|
+
function splitProviderCredentialFields(provider, rawConfig) {
|
|
109
110
|
const fields = PROVIDER_FIELD_SPLITS[provider];
|
|
110
111
|
return {
|
|
111
112
|
credentials: copyKnownFields(rawConfig, fields.credentials),
|
|
@@ -336,7 +337,7 @@ function readLegacyAgentProviderCredentials(input) {
|
|
|
336
337
|
if (!legacyProviderHasCredentialData(providerKey, rawProviderConfig)) {
|
|
337
338
|
continue;
|
|
338
339
|
}
|
|
339
|
-
const { credentials, config } =
|
|
340
|
+
const { credentials, config } = splitProviderCredentialFields(providerKey, rawProviderConfig);
|
|
340
341
|
candidates.push({
|
|
341
342
|
provider: providerKey,
|
|
342
343
|
credentials,
|
|
@@ -13,6 +13,7 @@ const github_copilot_1 = require("./providers/github-copilot");
|
|
|
13
13
|
const auth_flow_1 = require("./auth/auth-flow");
|
|
14
14
|
const provider_models_1 = require("./provider-models");
|
|
15
15
|
const runtime_1 = require("../nerves/runtime");
|
|
16
|
+
const provider_attempt_1 = require("./provider-attempt");
|
|
16
17
|
const PING_TIMEOUT_MS = 10_000;
|
|
17
18
|
const PING_PROMPT = "ping";
|
|
18
19
|
const CHAT_PING_MAX_TOKENS = 1;
|
|
@@ -67,6 +68,30 @@ function sanitizeErrorMessage(message) {
|
|
|
67
68
|
// Already clean (e.g., "401 Provided authentication token is expired.")
|
|
68
69
|
return message;
|
|
69
70
|
}
|
|
71
|
+
async function readGithubCopilotModelPingError(response) {
|
|
72
|
+
let detail = `HTTP ${response.status}`;
|
|
73
|
+
try {
|
|
74
|
+
const json = await response.json();
|
|
75
|
+
/* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
|
|
76
|
+
if (typeof json.error === "string")
|
|
77
|
+
detail = json.error;
|
|
78
|
+
else if (typeof json.error === "object" && json.error !== null) {
|
|
79
|
+
const errObj = json.error;
|
|
80
|
+
if (typeof errObj.message === "string")
|
|
81
|
+
detail = errObj.message;
|
|
82
|
+
}
|
|
83
|
+
else if (typeof json.message === "string")
|
|
84
|
+
detail = json.message;
|
|
85
|
+
/* v8 ignore stop */
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// response body not JSON — keep HTTP status
|
|
89
|
+
}
|
|
90
|
+
return detail;
|
|
91
|
+
}
|
|
92
|
+
function createStatusError(message, status) {
|
|
93
|
+
return Object.assign(new Error(message), { status });
|
|
94
|
+
}
|
|
70
95
|
async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
|
|
71
96
|
const base = baseUrl.replace(/\/+$/, "");
|
|
72
97
|
const isClaude = model.startsWith("claude");
|
|
@@ -74,41 +99,31 @@ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch)
|
|
|
74
99
|
const body = isClaude
|
|
75
100
|
? JSON.stringify(createChatPingRequest(model))
|
|
76
101
|
: JSON.stringify(createResponsePingRequest(model));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
const attempt = await (0, provider_attempt_1.runProviderAttempt)({
|
|
103
|
+
operation: "model-ping",
|
|
104
|
+
provider: "github-copilot",
|
|
105
|
+
model,
|
|
106
|
+
classifyError: github_copilot_1.classifyGithubCopilotError,
|
|
107
|
+
policy: {
|
|
108
|
+
maxAttempts: 3,
|
|
109
|
+
baseDelayMs: 0,
|
|
110
|
+
backoffMultiplier: 2,
|
|
111
|
+
},
|
|
112
|
+
run: async () => {
|
|
113
|
+
const response = await fetchImpl(url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${token}`,
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body,
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw createStatusError(await readGithubCopilotModelPingError(response), response.status);
|
|
98
123
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// response body not JSON — keep HTTP status
|
|
105
|
-
}
|
|
106
|
-
return { ok: false, error: detail };
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
/* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
|
|
110
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
111
|
-
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return attempt.ok ? { ok: true } : { ok: false, error: attempt.error.message };
|
|
112
127
|
}
|
|
113
128
|
function hasEmptyCredentials(provider, config) {
|
|
114
129
|
const record = config;
|
|
@@ -121,37 +136,37 @@ function hasEmptyCredentials(provider, config) {
|
|
|
121
136
|
}
|
|
122
137
|
return identity_1.PROVIDER_CREDENTIALS[provider].required.some((key) => !record[key]);
|
|
123
138
|
}
|
|
124
|
-
function createRuntimeForPing(provider, config) {
|
|
139
|
+
function createRuntimeForPing(provider, config, model) {
|
|
125
140
|
// Use the same provider defaults as auth switch and hatch so verification
|
|
126
141
|
// cannot drift to stale provider/model pairings, and pass the checked
|
|
127
142
|
// credentials directly so daemon-side pings do not depend on --agent globals.
|
|
128
|
-
const
|
|
143
|
+
const resolvedModel = model ?? (0, provider_models_1.getDefaultModelForProvider)(provider);
|
|
129
144
|
switch (provider) {
|
|
130
145
|
case "anthropic":
|
|
131
|
-
return (0, anthropic_1.createAnthropicProviderRuntime)(
|
|
146
|
+
return (0, anthropic_1.createAnthropicProviderRuntime)(resolvedModel, config);
|
|
132
147
|
case "azure":
|
|
133
|
-
return (0, azure_1.createAzureProviderRuntime)(
|
|
148
|
+
return (0, azure_1.createAzureProviderRuntime)(resolvedModel, {
|
|
134
149
|
...config,
|
|
135
150
|
apiVersion: config.apiVersion ?? DEFAULT_AZURE_API_VERSION,
|
|
136
151
|
});
|
|
137
152
|
case "minimax":
|
|
138
|
-
return (0, minimax_1.createMinimaxProviderRuntime)(
|
|
153
|
+
return (0, minimax_1.createMinimaxProviderRuntime)(resolvedModel, config);
|
|
139
154
|
case "openai-codex":
|
|
140
|
-
return (0, openai_codex_1.createOpenAICodexProviderRuntime)(
|
|
155
|
+
return (0, openai_codex_1.createOpenAICodexProviderRuntime)(resolvedModel, config);
|
|
141
156
|
case "github-copilot":
|
|
142
|
-
return (0, github_copilot_1.createGithubCopilotProviderRuntime)(
|
|
157
|
+
return (0, github_copilot_1.createGithubCopilotProviderRuntime)(resolvedModel, config);
|
|
143
158
|
/* v8 ignore next 2 -- exhaustive: all providers handled above @preserve */
|
|
144
159
|
default:
|
|
145
160
|
throw new Error(`unsupported provider for ping: ${provider}`);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
|
-
async function pingProvider(provider, config) {
|
|
163
|
+
async function pingProvider(provider, config, options = {}) {
|
|
149
164
|
if (hasEmptyCredentials(provider, config)) {
|
|
150
165
|
return { ok: false, classification: "auth-failure", message: "no credentials configured" };
|
|
151
166
|
}
|
|
152
167
|
let runtime;
|
|
153
168
|
try {
|
|
154
|
-
runtime = createRuntimeForPing(provider, config);
|
|
169
|
+
runtime = createRuntimeForPing(provider, config, options.model);
|
|
155
170
|
/* v8 ignore start -- factory creation failure: tested via individual provider init tests @preserve */
|
|
156
171
|
}
|
|
157
172
|
catch (error) {
|
|
@@ -162,58 +177,67 @@ async function pingProvider(provider, config) {
|
|
|
162
177
|
};
|
|
163
178
|
}
|
|
164
179
|
/* v8 ignore stop */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
const attempt = await (0, provider_attempt_1.runProviderAttempt)({
|
|
181
|
+
operation: "ping",
|
|
182
|
+
provider,
|
|
183
|
+
model: runtime.model,
|
|
184
|
+
classifyError: (error) => runtime.classifyError(error),
|
|
185
|
+
policy: {
|
|
186
|
+
maxAttempts: 3,
|
|
187
|
+
baseDelayMs: 0,
|
|
188
|
+
backoffMultiplier: 2,
|
|
189
|
+
...options.attemptPolicy,
|
|
190
|
+
},
|
|
191
|
+
sleep: options.sleep,
|
|
192
|
+
run: async () => {
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
/* v8 ignore next -- timeout callback: only fires after 10s, tests resolve faster @preserve */
|
|
195
|
+
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT_MS);
|
|
196
|
+
try {
|
|
197
|
+
// Minimal API call — no thinking, no reasoning, no tools.
|
|
198
|
+
if (provider === "anthropic") {
|
|
199
|
+
// Use haiku for the ping — setup tokens may not have access to newer
|
|
200
|
+
// models, but if haiku works, the credentials are valid.
|
|
201
|
+
// Override the beta header to exclude thinking (which requires a
|
|
202
|
+
// thinking param in the request body).
|
|
203
|
+
const client = runtime.client;
|
|
204
|
+
await client.messages.create(createChatPingRequest(ANTHROPIC_SETUP_PING_MODEL), { signal: controller.signal, headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" } });
|
|
205
|
+
}
|
|
206
|
+
else if (provider === "openai-codex") {
|
|
207
|
+
await runtime.streamTurn({
|
|
208
|
+
messages: createPingMessages(),
|
|
209
|
+
activeTools: [],
|
|
210
|
+
callbacks: PING_CALLBACKS,
|
|
211
|
+
signal: controller.signal,
|
|
212
|
+
toolChoiceRequired: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// OpenAI-compatible providers (azure, minimax, github-copilot)
|
|
217
|
+
const client = runtime.client;
|
|
218
|
+
await client.chat.completions.create(createChatPingRequest(runtime.model), { signal: controller.signal });
|
|
219
|
+
}
|
|
178
220
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
messages: createPingMessages(),
|
|
182
|
-
activeTools: [],
|
|
183
|
-
callbacks: PING_CALLBACKS,
|
|
184
|
-
signal: controller.signal,
|
|
185
|
-
toolChoiceRequired: false,
|
|
186
|
-
});
|
|
221
|
+
finally {
|
|
222
|
+
clearTimeout(timeout);
|
|
187
223
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
return { ok: true };
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
clearTimeout(timeout);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
const err = error instanceof Error ? error : /* v8 ignore next -- defensive @preserve */ new Error(String(error));
|
|
201
|
-
let classification;
|
|
202
|
-
try {
|
|
203
|
-
classification = runtime.classifyError(err);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
/* v8 ignore next -- defensive: classifyError should not throw @preserve */
|
|
207
|
-
classification = "unknown";
|
|
208
|
-
}
|
|
209
|
-
(0, runtime_1.emitNervesEvent)({
|
|
210
|
-
component: "engine",
|
|
211
|
-
event: "engine.provider_ping_fail",
|
|
212
|
-
message: `provider ping failed: ${provider}`,
|
|
213
|
-
meta: { provider, classification, error: err.message },
|
|
214
|
-
});
|
|
215
|
-
return { ok: false, classification, message: sanitizeErrorMessage(err.message) };
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
if (attempt.ok) {
|
|
227
|
+
return { ok: true, attempts: attempt.attempts };
|
|
216
228
|
}
|
|
229
|
+
(0, runtime_1.emitNervesEvent)({
|
|
230
|
+
component: "engine",
|
|
231
|
+
event: "engine.provider_ping_fail",
|
|
232
|
+
message: `provider ping failed: ${provider}`,
|
|
233
|
+
meta: { provider, classification: attempt.classification, error: attempt.error.message },
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
classification: attempt.classification,
|
|
238
|
+
message: sanitizeErrorMessage(attempt.error.message),
|
|
239
|
+
attempts: attempt.attempts,
|
|
240
|
+
};
|
|
217
241
|
}
|
|
218
242
|
const PINGABLE_PROVIDERS = ["anthropic", "openai-codex", "azure", "minimax", "github-copilot"];
|
|
219
243
|
async function runHealthInventory(agentName, currentProvider, deps = {}) {
|
|
@@ -104,6 +104,10 @@ function enforceTrustGate(input) {
|
|
|
104
104
|
return { allowed: true };
|
|
105
105
|
}
|
|
106
106
|
// Open senses (BlueBubbles/iMessage) — enforce trust rules
|
|
107
|
+
// Group chat with a family member present — allow regardless of trust level
|
|
108
|
+
if (input.isGroupChat && input.groupHasFamilyMember) {
|
|
109
|
+
return { allowed: true };
|
|
110
|
+
}
|
|
107
111
|
const trustLevel = input.friend.trustLevel ?? "friend";
|
|
108
112
|
// Family and friend — always allow on open
|
|
109
113
|
if ((0, types_1.isTrustedLevel)(trustLevel)) {
|
|
@@ -119,11 +123,7 @@ function enforceTrustGate(input) {
|
|
|
119
123
|
return handleStranger(input, bundleRoot, nowIso);
|
|
120
124
|
}
|
|
121
125
|
function handleAcquaintance(input, bundleRoot, nowIso) {
|
|
122
|
-
const { isGroupChat,
|
|
123
|
-
// Group chat with family member present — allow
|
|
124
|
-
if (isGroupChat && groupHasFamilyMember) {
|
|
125
|
-
return { allowed: true };
|
|
126
|
-
}
|
|
126
|
+
const { isGroupChat, hasExistingGroupWithFamily } = input;
|
|
127
127
|
let result;
|
|
128
128
|
let noticeDetail;
|
|
129
129
|
if (isGroupChat) {
|