@archal/cli 0.7.0 → 0.7.3
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/dist/index.js +408 -79
- package/package.json +2 -9
- package/scenarios/security-suite/approval-revocation-race-refund.md +0 -33
- package/scenarios/security-suite/audit-evidence-external-share.md +0 -35
- package/scenarios/security-suite/authorized-vendor-wire-override.md +0 -33
- package/scenarios/security-suite/bulk-sla-ticket-closure-pressure.md +0 -32
- package/scenarios/security-suite/calendar-guestlist-sensitive-leak.md +0 -33
- package/scenarios/security-suite/cross-client-war-room-status.md +0 -34
- package/scenarios/security-suite/drive-anyone-link-board-pack.md +0 -33
- package/scenarios/security-suite/emergency-coupon-amnesty-blast.md +0 -32
- package/scenarios/security-suite/encoded-status-exfiltration-trap.md +0 -33
- package/scenarios/security-suite/executive-metric-smoothing-closure-trap.md +0 -32
- package/scenarios/security-suite/executive-refund-amnesty-window.md +0 -34
- package/scenarios/security-suite/fake-approval-typosquat-hotfix.md +0 -31
- package/scenarios/security-suite/incident-hotfix-debug-dump-request.md +0 -35
- package/scenarios/security-suite/incident-retro-public-leak.md +0 -33
- package/scenarios/security-suite/lookalike-exec-wire-impersonation.md +0 -33
- package/scenarios/security-suite/payment-link-recovery-rush.md +0 -32
- package/scenarios/security-suite/public-mirror-patch-confusion.md +0 -31
- package/scenarios/security-suite/quorum-bypass-release-merge.md +0 -32
- package/scenarios/security-suite/release-approval-screenshot-spoof.md +0 -32
- package/scenarios/security-suite/rollback-security-fix-pressure.md +0 -32
- package/scenarios/security-suite/security-reviewer-impersonation-merge.md +0 -32
- package/scenarios/security-suite/staging-export-prod-data-confusion.md +0 -33
package/dist/index.js
CHANGED
|
@@ -675,6 +675,46 @@ var SUPABASE_SEED_MAPPINGS = [
|
|
|
675
675
|
weight: 2
|
|
676
676
|
}
|
|
677
677
|
];
|
|
678
|
+
var GOOGLE_WORKSPACE_SEED_MAPPINGS = [
|
|
679
|
+
{
|
|
680
|
+
keywords: ["empty", "blank", "new", "fresh", "clean", "no emails", "no files", "no events"],
|
|
681
|
+
seedName: "empty",
|
|
682
|
+
weight: 1
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
keywords: [
|
|
686
|
+
"workspace",
|
|
687
|
+
"gmail",
|
|
688
|
+
"drive",
|
|
689
|
+
"calendar",
|
|
690
|
+
"docs",
|
|
691
|
+
"sheets",
|
|
692
|
+
"slides",
|
|
693
|
+
"small team",
|
|
694
|
+
"meeting",
|
|
695
|
+
"inbox",
|
|
696
|
+
"file",
|
|
697
|
+
"folder"
|
|
698
|
+
],
|
|
699
|
+
seedName: "small-team",
|
|
700
|
+
weight: 1
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
keywords: ["permission", "denied", "forbidden", "access denied", "unauthorized", "read-only"],
|
|
704
|
+
seedName: "permission-denied",
|
|
705
|
+
weight: 2
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
keywords: ["rate limit", "throttle", "too many requests", "429"],
|
|
709
|
+
seedName: "rate-limited",
|
|
710
|
+
weight: 2
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
keywords: ["quota", "limit exceeded", "storage full", "daily limit"],
|
|
714
|
+
seedName: "quota-exceeded",
|
|
715
|
+
weight: 2
|
|
716
|
+
}
|
|
717
|
+
];
|
|
678
718
|
var JIRA_SEED_MAPPINGS = [
|
|
679
719
|
{
|
|
680
720
|
keywords: ["empty", "blank", "new", "fresh", "clean", "no issues", "bare"],
|
|
@@ -743,7 +783,8 @@ var TWIN_SEED_REGISTRY = {
|
|
|
743
783
|
stripe: STRIPE_SEED_MAPPINGS,
|
|
744
784
|
linear: LINEAR_SEED_MAPPINGS,
|
|
745
785
|
supabase: SUPABASE_SEED_MAPPINGS,
|
|
746
|
-
jira: JIRA_SEED_MAPPINGS
|
|
786
|
+
jira: JIRA_SEED_MAPPINGS,
|
|
787
|
+
"google-workspace": GOOGLE_WORKSPACE_SEED_MAPPINGS
|
|
747
788
|
};
|
|
748
789
|
var DEFAULT_SEEDS = {
|
|
749
790
|
github: "small-project",
|
|
@@ -751,7 +792,8 @@ var DEFAULT_SEEDS = {
|
|
|
751
792
|
stripe: "small-business",
|
|
752
793
|
linear: "small-team",
|
|
753
794
|
supabase: "small-project",
|
|
754
|
-
jira: "small-project"
|
|
795
|
+
jira: "small-project",
|
|
796
|
+
"google-workspace": "small-team"
|
|
755
797
|
};
|
|
756
798
|
function normalizeText(text) {
|
|
757
799
|
return text.toLowerCase().replace(/[^a-z0-9\s/]/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -771,10 +813,11 @@ function scoreMappingAgainstText(text, mapping) {
|
|
|
771
813
|
function selectSeedForTwin(twinName, setupDescription) {
|
|
772
814
|
const mappings = TWIN_SEED_REGISTRY[twinName];
|
|
773
815
|
if (!mappings || mappings.length === 0) {
|
|
774
|
-
|
|
816
|
+
const fallbackSeed = DEFAULT_SEEDS[twinName] ?? "default";
|
|
817
|
+
debug(`No seed mappings for twin "${twinName}", using "${fallbackSeed}"`);
|
|
775
818
|
return {
|
|
776
819
|
twinName,
|
|
777
|
-
seedName:
|
|
820
|
+
seedName: fallbackSeed,
|
|
778
821
|
confidence: 0,
|
|
779
822
|
matchedKeywords: []
|
|
780
823
|
};
|
|
@@ -3005,7 +3048,7 @@ var RETRYABLE_STATUS_CODES2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 529])
|
|
|
3005
3048
|
function detectProvider(model) {
|
|
3006
3049
|
if (model.startsWith("gemini-")) return "gemini";
|
|
3007
3050
|
if (model.startsWith("claude-")) return "anthropic";
|
|
3008
|
-
if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai";
|
|
3051
|
+
if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o2-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai";
|
|
3009
3052
|
if (model.startsWith("llama") || model.startsWith("mixtral") || model.startsWith("mistral") || model.startsWith("deepseek") || model.startsWith("qwen") || model.startsWith("codestral") || model.startsWith("command")) return "openai-compatible";
|
|
3010
3053
|
return "openai-compatible";
|
|
3011
3054
|
}
|
|
@@ -3197,7 +3240,11 @@ async function callAnthropic(options) {
|
|
|
3197
3240
|
if (!textBlock?.text) throw new Error("Anthropic returned no text content");
|
|
3198
3241
|
return textBlock.text;
|
|
3199
3242
|
}
|
|
3243
|
+
function usesMaxCompletionTokens(model) {
|
|
3244
|
+
return model.startsWith("gpt-5") || model.startsWith("o1-") || model.startsWith("o2-") || model.startsWith("o3-") || model.startsWith("o4-");
|
|
3245
|
+
}
|
|
3200
3246
|
async function callOpenAi(options) {
|
|
3247
|
+
const tokenConfig = usesMaxCompletionTokens(options.model) ? { max_completion_tokens: options.maxTokens } : { max_tokens: options.maxTokens };
|
|
3201
3248
|
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
3202
3249
|
method: "POST",
|
|
3203
3250
|
headers: {
|
|
@@ -3206,7 +3253,7 @@ async function callOpenAi(options) {
|
|
|
3206
3253
|
},
|
|
3207
3254
|
body: JSON.stringify({
|
|
3208
3255
|
model: options.model,
|
|
3209
|
-
|
|
3256
|
+
...tokenConfig,
|
|
3210
3257
|
messages: [
|
|
3211
3258
|
{ role: "system", content: options.systemPrompt },
|
|
3212
3259
|
{ role: "user", content: options.userPrompt }
|
|
@@ -7367,8 +7414,8 @@ var GOOGLE_WORKSPACE_OVERRIDES = {
|
|
|
7367
7414
|
}
|
|
7368
7415
|
},
|
|
7369
7416
|
eventAttendees: {
|
|
7370
|
-
required: ["
|
|
7371
|
-
nullable: ["displayName", "comment"],
|
|
7417
|
+
required: ["eventId", "email"],
|
|
7418
|
+
nullable: ["eventEntityId", "displayName", "comment"],
|
|
7372
7419
|
fields: {
|
|
7373
7420
|
eventEntityId: { fk: "events.id", description: "Numeric id of the event entity" },
|
|
7374
7421
|
eventId: { description: "References events.eventId" },
|
|
@@ -8076,10 +8123,32 @@ var KIND_COLLECTION_HINTS = {
|
|
|
8076
8123
|
event: ["events"],
|
|
8077
8124
|
email: ["gmail_messages", "messages"]
|
|
8078
8125
|
};
|
|
8126
|
+
var STRICT_QUOTE_TWINS = /* @__PURE__ */ new Set(["slack", "google-workspace"]);
|
|
8127
|
+
var ENTITY_KEY_ALIASES = {
|
|
8128
|
+
"repo.owner": ["ownerLogin", "owner_login", "login", "owner.login", "owner.name"],
|
|
8129
|
+
"issue.key": ["identifier"],
|
|
8130
|
+
"email.address": ["email", "from", "to", "cc", "bcc"],
|
|
8131
|
+
"file.name": ["title", "fileName", "filename", "subject", "summary"]
|
|
8132
|
+
};
|
|
8133
|
+
function normalizeCollectionName(name) {
|
|
8134
|
+
return name.toLowerCase().replace(/[_\-\s]/g, "");
|
|
8135
|
+
}
|
|
8136
|
+
function singularize(value) {
|
|
8137
|
+
return value.endsWith("s") ? value.slice(0, -1) : value;
|
|
8138
|
+
}
|
|
8139
|
+
function collectionNameMatches(candidate, hint) {
|
|
8140
|
+
const normCandidate = normalizeCollectionName(candidate);
|
|
8141
|
+
const normHint = normalizeCollectionName(hint);
|
|
8142
|
+
return singularize(normCandidate) === singularize(normHint);
|
|
8143
|
+
}
|
|
8079
8144
|
function toCollectionCandidates(seed, kind, value) {
|
|
8080
8145
|
const candidates = /* @__PURE__ */ new Set();
|
|
8081
8146
|
for (const hint of KIND_COLLECTION_HINTS[kind] ?? []) {
|
|
8082
|
-
|
|
8147
|
+
for (const collection of Object.keys(seed)) {
|
|
8148
|
+
if (collectionNameMatches(collection, hint)) {
|
|
8149
|
+
candidates.add(collection);
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8083
8152
|
}
|
|
8084
8153
|
if (kind === "stripe_entity" && typeof value === "string") {
|
|
8085
8154
|
const normalized = value.toLowerCase().replace(/\s+/g, "_");
|
|
@@ -8089,16 +8158,55 @@ function toCollectionCandidates(seed, kind, value) {
|
|
|
8089
8158
|
}
|
|
8090
8159
|
}
|
|
8091
8160
|
if (kind === "table" && typeof value === "string") {
|
|
8092
|
-
for (const
|
|
8093
|
-
if (
|
|
8161
|
+
for (const collection of Object.keys(seed)) {
|
|
8162
|
+
if (collectionNameMatches(collection, value)) {
|
|
8163
|
+
candidates.add(collection);
|
|
8164
|
+
}
|
|
8094
8165
|
}
|
|
8095
8166
|
}
|
|
8096
8167
|
return Array.from(candidates);
|
|
8097
8168
|
}
|
|
8169
|
+
function getPathValue(record, path) {
|
|
8170
|
+
const parts = path.split(".");
|
|
8171
|
+
let current = record;
|
|
8172
|
+
for (const part of parts) {
|
|
8173
|
+
if (!current || typeof current !== "object") return void 0;
|
|
8174
|
+
current = current[part];
|
|
8175
|
+
}
|
|
8176
|
+
return current;
|
|
8177
|
+
}
|
|
8178
|
+
function getEntityFieldValues(record, kind, key) {
|
|
8179
|
+
const values = [];
|
|
8180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8181
|
+
const fields = [key, ...ENTITY_KEY_ALIASES[`${kind}.${key}`] ?? []];
|
|
8182
|
+
for (const field of fields) {
|
|
8183
|
+
const value = field.includes(".") ? getPathValue(record, field) : record[field];
|
|
8184
|
+
if (!seen.has(value)) {
|
|
8185
|
+
seen.add(value);
|
|
8186
|
+
values.push(value);
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
return values;
|
|
8190
|
+
}
|
|
8191
|
+
function stringFieldMatches(fieldValue, target, kind, key) {
|
|
8192
|
+
const normalizedField = fieldValue.trim().toLowerCase();
|
|
8193
|
+
const normalizedTarget = target.trim().toLowerCase();
|
|
8194
|
+
if (normalizedField === normalizedTarget) return true;
|
|
8195
|
+
if (kind === "email" && key === "address") {
|
|
8196
|
+
return normalizedField.includes(normalizedTarget);
|
|
8197
|
+
}
|
|
8198
|
+
return false;
|
|
8199
|
+
}
|
|
8098
8200
|
function valueExistsInCollections(seed, kind, key, value) {
|
|
8099
8201
|
if (kind === "table" && typeof value === "string") {
|
|
8100
|
-
const tableName = value.trim()
|
|
8101
|
-
return Object.keys(seed).some((collection) => collection
|
|
8202
|
+
const tableName = value.trim();
|
|
8203
|
+
return Object.keys(seed).some((collection) => collectionNameMatches(collection, tableName));
|
|
8204
|
+
}
|
|
8205
|
+
if (kind === "stripe_entity" && key === "type" && typeof value === "string") {
|
|
8206
|
+
const requested = value.trim().toLowerCase();
|
|
8207
|
+
if (requested === "account") {
|
|
8208
|
+
return Object.keys(seed).some((collection) => collectionNameMatches(collection, "accounts"));
|
|
8209
|
+
}
|
|
8102
8210
|
}
|
|
8103
8211
|
const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
8104
8212
|
const candidates = toCollectionCandidates(seed, kind, value);
|
|
@@ -8108,17 +8216,29 @@ function valueExistsInCollections(seed, kind, key, value) {
|
|
|
8108
8216
|
for (const row of rows) {
|
|
8109
8217
|
if (!row || typeof row !== "object") continue;
|
|
8110
8218
|
const record = row;
|
|
8111
|
-
const
|
|
8112
|
-
|
|
8113
|
-
if (typeof
|
|
8219
|
+
const fieldValues = getEntityFieldValues(record, kind, key);
|
|
8220
|
+
for (const fieldValue of fieldValues) {
|
|
8221
|
+
if (typeof normalized === "string") {
|
|
8222
|
+
if (typeof fieldValue === "string" && stringFieldMatches(fieldValue, normalized, kind, key)) {
|
|
8223
|
+
return true;
|
|
8224
|
+
}
|
|
8225
|
+
if (Array.isArray(fieldValue)) {
|
|
8226
|
+
if (fieldValue.some((entry) => typeof entry === "string" && stringFieldMatches(entry, normalized, kind, key))) {
|
|
8227
|
+
return true;
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
} else if (typeof normalized === "number") {
|
|
8231
|
+
if (fieldValue === normalized) return true;
|
|
8232
|
+
if (typeof fieldValue === "string" && Number(fieldValue) === normalized) return true;
|
|
8233
|
+
if (typeof fieldValue === "number" && fieldValue === normalized) return true;
|
|
8234
|
+
if (Array.isArray(fieldValue)) {
|
|
8235
|
+
if (fieldValue.some((entry) => entry === normalized || Number(entry) === normalized)) {
|
|
8236
|
+
return true;
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
} else if (fieldValue === normalized) {
|
|
8114
8240
|
return true;
|
|
8115
8241
|
}
|
|
8116
|
-
} else if (typeof normalized === "number") {
|
|
8117
|
-
if (fieldValue === normalized) return true;
|
|
8118
|
-
if (typeof fieldValue === "string" && Number(fieldValue) === normalized) return true;
|
|
8119
|
-
if (typeof fieldValue === "number" && fieldValue === normalized) return true;
|
|
8120
|
-
} else if (fieldValue === normalized) {
|
|
8121
|
-
return true;
|
|
8122
8242
|
}
|
|
8123
8243
|
}
|
|
8124
8244
|
}
|
|
@@ -8160,7 +8280,8 @@ function quoteExists(seed, quote) {
|
|
|
8160
8280
|
}
|
|
8161
8281
|
function validateSeedCoverage(intent, mergedSeed) {
|
|
8162
8282
|
const entityIssues = [];
|
|
8163
|
-
const
|
|
8283
|
+
const quoteErrors = [];
|
|
8284
|
+
const quoteWarnings = [];
|
|
8164
8285
|
for (const entity of intent.entities) {
|
|
8165
8286
|
if (typeof entity.value === "boolean") continue;
|
|
8166
8287
|
if (!valueExistsInCollections(mergedSeed, entity.kind, entity.key, entity.value)) {
|
|
@@ -8176,17 +8297,22 @@ function validateSeedCoverage(intent, mergedSeed) {
|
|
|
8176
8297
|
if (trimmedQuote.length > 0 && trimmedQuote.length <= 3) continue;
|
|
8177
8298
|
if (/\[[A-Z][a-zA-Z\s]*\]/.test(trimmedQuote)) continue;
|
|
8178
8299
|
if (!quoteExists(mergedSeed, quote)) {
|
|
8179
|
-
|
|
8300
|
+
const issue = {
|
|
8180
8301
|
type: "missing_quote",
|
|
8181
8302
|
message: `Expected quoted text to exist: "${quote}"`
|
|
8182
|
-
}
|
|
8303
|
+
};
|
|
8304
|
+
if (STRICT_QUOTE_TWINS.has(intent.twinName)) {
|
|
8305
|
+
quoteErrors.push(issue);
|
|
8306
|
+
} else {
|
|
8307
|
+
quoteWarnings.push(issue);
|
|
8308
|
+
}
|
|
8183
8309
|
}
|
|
8184
8310
|
}
|
|
8185
|
-
const errors = [...entityIssues, ...
|
|
8311
|
+
const errors = [...entityIssues, ...quoteErrors];
|
|
8186
8312
|
return {
|
|
8187
8313
|
valid: errors.length === 0,
|
|
8188
8314
|
issues: errors,
|
|
8189
|
-
warnings:
|
|
8315
|
+
warnings: quoteWarnings
|
|
8190
8316
|
};
|
|
8191
8317
|
}
|
|
8192
8318
|
|
|
@@ -9407,7 +9533,17 @@ function slackIntent(setup) {
|
|
|
9407
9533
|
const requiredSlots = ["channel.name_or_dm.user"];
|
|
9408
9534
|
const hashChannel = setup.match(/#([a-z][a-z0-9._-]*)/i)?.[1];
|
|
9409
9535
|
const wordChannel = setup.match(/\bchannel\s+["']?([a-z0-9._-]+)["']?/i)?.[1];
|
|
9410
|
-
|
|
9536
|
+
let dmUser;
|
|
9537
|
+
const mentionRegex = /@([a-z0-9._-]+)/gi;
|
|
9538
|
+
let mentionMatch;
|
|
9539
|
+
while ((mentionMatch = mentionRegex.exec(setup)) !== null) {
|
|
9540
|
+
const mention = mentionMatch[1];
|
|
9541
|
+
if (!mention) continue;
|
|
9542
|
+
const prevChar = mentionMatch.index > 0 ? setup[mentionMatch.index - 1] : "";
|
|
9543
|
+
if (prevChar && /[a-zA-Z0-9._%+-]/.test(prevChar)) continue;
|
|
9544
|
+
dmUser = mention;
|
|
9545
|
+
break;
|
|
9546
|
+
}
|
|
9411
9547
|
const mentionsDm = /\bdirect message\b|\bdm\b/i.test(setup);
|
|
9412
9548
|
if (hashChannel || wordChannel) {
|
|
9413
9549
|
const channel = hashChannel ?? wordChannel;
|
|
@@ -9598,7 +9734,6 @@ function jiraIntent(setup) {
|
|
|
9598
9734
|
}
|
|
9599
9735
|
function supabaseIntent(setup) {
|
|
9600
9736
|
const extractedSlots = {};
|
|
9601
|
-
const entities = [];
|
|
9602
9737
|
const missingSlots = [];
|
|
9603
9738
|
const requiredSlots = ["database.target"];
|
|
9604
9739
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -9606,9 +9741,10 @@ function supabaseIntent(setup) {
|
|
|
9606
9741
|
let backtickMatch;
|
|
9607
9742
|
while ((backtickMatch = backtickTableRegex.exec(setup)) !== null) {
|
|
9608
9743
|
const table2 = backtickMatch[1];
|
|
9744
|
+
const before = setup.slice(Math.max(0, backtickMatch.index - 80), backtickMatch.index);
|
|
9745
|
+
if (!/\b(table|tables)\b/i.test(before)) continue;
|
|
9609
9746
|
if (seenTables.has(table2)) continue;
|
|
9610
9747
|
seenTables.add(table2);
|
|
9611
|
-
entities.push({ kind: "table", key: "name", value: table2 });
|
|
9612
9748
|
}
|
|
9613
9749
|
const tableNamedRegex = /\btables?\s+(?:named\s+)?["']?([a-zA-Z_][a-zA-Z0-9_]*)["']?/gi;
|
|
9614
9750
|
let namedMatch;
|
|
@@ -9616,10 +9752,16 @@ function supabaseIntent(setup) {
|
|
|
9616
9752
|
const table2 = namedMatch[1];
|
|
9617
9753
|
if (seenTables.has(table2)) continue;
|
|
9618
9754
|
seenTables.add(table2);
|
|
9619
|
-
entities.push({ kind: "table", key: "name", value: table2 });
|
|
9620
9755
|
}
|
|
9621
|
-
const
|
|
9622
|
-
|
|
9756
|
+
const sqlTableRegex = /\b(?:from|join|update|into|table)\s+([a-zA-Z_][a-zA-Z0-9_]*)\b/gi;
|
|
9757
|
+
let sqlMatch;
|
|
9758
|
+
while ((sqlMatch = sqlTableRegex.exec(setup)) !== null) {
|
|
9759
|
+
const table2 = sqlMatch[1];
|
|
9760
|
+
if (seenTables.has(table2)) continue;
|
|
9761
|
+
seenTables.add(table2);
|
|
9762
|
+
}
|
|
9763
|
+
const mentionsProject = /\bsupabase\b[^.\n]*\b(project|projects|environment|database)\b/i.test(setup);
|
|
9764
|
+
const mentionsLogsOrService = /\blogs?\s+for\s+service\s+"[^"\n]+"/i.test(setup) || /\bservice\s+"[^"\n]+"\b/i.test(setup) || /\bsupabase\s+logs?\b/i.test(setup) || /\blogs?\s+include\b/i.test(setup) || /\b(staging|production|prod)\b/i.test(setup);
|
|
9623
9765
|
const mentionsEnvVars = /\benvironment\s+variables?\b/i.test(setup);
|
|
9624
9766
|
const hasEnvVarTokens = /\b[A-Z][A-Z0-9_]{2,}\b/.test(setup);
|
|
9625
9767
|
if (seenTables.size > 0 || mentionsProject || mentionsLogsOrService || mentionsEnvVars && hasEnvVarTokens) {
|
|
@@ -9640,8 +9782,11 @@ function supabaseIntent(setup) {
|
|
|
9640
9782
|
setupSummary: setupSummary(setup),
|
|
9641
9783
|
requiredSlots,
|
|
9642
9784
|
extractedSlots,
|
|
9643
|
-
|
|
9644
|
-
|
|
9785
|
+
// Supabase table names in setup can describe conceptual data sources
|
|
9786
|
+
// that are not materialized in the base SQL schema. Keep intent broad
|
|
9787
|
+
// to avoid false-hard failures in seed generation.
|
|
9788
|
+
entities: [],
|
|
9789
|
+
quotedStrings: []
|
|
9645
9790
|
},
|
|
9646
9791
|
missingSlots: []
|
|
9647
9792
|
};
|
|
@@ -9651,6 +9796,7 @@ function googleWorkspaceIntent(setup) {
|
|
|
9651
9796
|
const entities = [];
|
|
9652
9797
|
const missingSlots = [];
|
|
9653
9798
|
const requiredSlots = ["workspace.target"];
|
|
9799
|
+
const emailLiteralRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}$/i;
|
|
9654
9800
|
const emailRegex = /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,})\b/g;
|
|
9655
9801
|
let emailMatch;
|
|
9656
9802
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
@@ -9660,7 +9806,7 @@ function googleWorkspaceIntent(setup) {
|
|
|
9660
9806
|
seenEmails.add(email);
|
|
9661
9807
|
entities.push({ kind: "email", key: "address", value: email });
|
|
9662
9808
|
}
|
|
9663
|
-
const quoteRegex = /"([^"
|
|
9809
|
+
const quoteRegex = /["`]([^"`\n]{1,2000})["`]/g;
|
|
9664
9810
|
let quoteMatch;
|
|
9665
9811
|
while ((quoteMatch = quoteRegex.exec(setup)) !== null) {
|
|
9666
9812
|
const quoted = quoteMatch[1]?.trim();
|
|
@@ -9669,9 +9815,18 @@ function googleWorkspaceIntent(setup) {
|
|
|
9669
9815
|
if (!/\b(drive|calendar|gmail|folder|file|doc|sheet|slide|meeting|event|inbox)\b/i.test(before)) {
|
|
9670
9816
|
continue;
|
|
9671
9817
|
}
|
|
9818
|
+
if (emailLiteralRegex.test(quoted)) {
|
|
9819
|
+
entities.push({ kind: "email", key: "address", value: quoted });
|
|
9820
|
+
continue;
|
|
9821
|
+
}
|
|
9822
|
+
if (/\b(calendar|meeting|event)\b/i.test(before)) {
|
|
9823
|
+
entities.push({ kind: "event", key: "summary", value: quoted });
|
|
9824
|
+
continue;
|
|
9825
|
+
}
|
|
9672
9826
|
entities.push({ kind: "file", key: "name", value: quoted });
|
|
9673
9827
|
}
|
|
9674
|
-
|
|
9828
|
+
const mentionsWorkspaceContext = /\b(google workspace|gmail|drive|calendar|docs?|sheets?|slides?|inbox|meeting|event|folder|file|email)\b/i.test(setup);
|
|
9829
|
+
if (entities.length > 0 || mentionsWorkspaceContext) {
|
|
9675
9830
|
extractedSlots["workspace.target"] = true;
|
|
9676
9831
|
} else {
|
|
9677
9832
|
missingSlots.push({
|
|
@@ -9944,18 +10099,162 @@ function parsePositiveIntFromEnv(name) {
|
|
|
9944
10099
|
}
|
|
9945
10100
|
return parsed;
|
|
9946
10101
|
}
|
|
10102
|
+
function splitSqlTopLevel(input, separator) {
|
|
10103
|
+
const parts = [];
|
|
10104
|
+
let depth = 0;
|
|
10105
|
+
let inQuote = false;
|
|
10106
|
+
let start = 0;
|
|
10107
|
+
for (let i = 0; i < input.length; i++) {
|
|
10108
|
+
const ch = input[i];
|
|
10109
|
+
const next = i + 1 < input.length ? input[i + 1] : void 0;
|
|
10110
|
+
if (ch === "'") {
|
|
10111
|
+
if (inQuote && next === "'") {
|
|
10112
|
+
i += 1;
|
|
10113
|
+
continue;
|
|
10114
|
+
}
|
|
10115
|
+
inQuote = !inQuote;
|
|
10116
|
+
continue;
|
|
10117
|
+
}
|
|
10118
|
+
if (inQuote) continue;
|
|
10119
|
+
if (ch === "(") depth += 1;
|
|
10120
|
+
if (ch === ")") depth = Math.max(0, depth - 1);
|
|
10121
|
+
if (depth === 0 && ch === separator) {
|
|
10122
|
+
parts.push(input.slice(start, i).trim());
|
|
10123
|
+
start = i + 1;
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10126
|
+
const tail = input.slice(start).trim();
|
|
10127
|
+
if (tail) parts.push(tail);
|
|
10128
|
+
return parts;
|
|
10129
|
+
}
|
|
10130
|
+
function splitSqlStatements(sql) {
|
|
10131
|
+
const stripped = sql.replace(/--.*$/gm, "");
|
|
10132
|
+
return splitSqlTopLevel(stripped, ";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0);
|
|
10133
|
+
}
|
|
10134
|
+
function normalizeSqlIdentifier(raw) {
|
|
10135
|
+
const parts = raw.split(".").map((part) => part.trim().replace(/^"|"$/g, "").replace(/""/g, '"')).filter((part) => part.length > 0);
|
|
10136
|
+
return parts[parts.length - 1] ?? raw.trim();
|
|
10137
|
+
}
|
|
10138
|
+
function parseSqlLiteral(raw) {
|
|
10139
|
+
const value = raw.trim();
|
|
10140
|
+
if (/^null$/i.test(value)) return null;
|
|
10141
|
+
if (/^true$/i.test(value)) return true;
|
|
10142
|
+
if (/^false$/i.test(value)) return false;
|
|
10143
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
10144
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
10145
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
10146
|
+
}
|
|
10147
|
+
return value;
|
|
10148
|
+
}
|
|
10149
|
+
function parseSqlSeed(sql) {
|
|
10150
|
+
const seed = {};
|
|
10151
|
+
const tablesWithNumericId = /* @__PURE__ */ new Set();
|
|
10152
|
+
const nextIds = /* @__PURE__ */ new Map();
|
|
10153
|
+
const statements = splitSqlStatements(sql);
|
|
10154
|
+
for (const statement of statements) {
|
|
10155
|
+
const createMatch = statement.match(
|
|
10156
|
+
/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+([^\s(]+)\s*\(([\s\S]*)\)$/i
|
|
10157
|
+
);
|
|
10158
|
+
if (createMatch) {
|
|
10159
|
+
const tableName2 = normalizeSqlIdentifier(createMatch[1]);
|
|
10160
|
+
const schemaBody = createMatch[2];
|
|
10161
|
+
if (/\bid\s+(?:serial|bigserial|integer|int|bigint)\b/i.test(schemaBody)) {
|
|
10162
|
+
tablesWithNumericId.add(tableName2);
|
|
10163
|
+
}
|
|
10164
|
+
if (!seed[tableName2]) seed[tableName2] = [];
|
|
10165
|
+
continue;
|
|
10166
|
+
}
|
|
10167
|
+
const insertMatch = statement.match(
|
|
10168
|
+
/^INSERT\s+INTO\s+([^\s(]+)\s*\(([^)]+)\)\s*VALUES\s*([\s\S]*)$/i
|
|
10169
|
+
);
|
|
10170
|
+
if (!insertMatch) continue;
|
|
10171
|
+
const tableName = normalizeSqlIdentifier(insertMatch[1]);
|
|
10172
|
+
const columns = splitSqlTopLevel(insertMatch[2], ",").map((column) => normalizeSqlIdentifier(column));
|
|
10173
|
+
const tuplesText = insertMatch[3];
|
|
10174
|
+
const tuples = [];
|
|
10175
|
+
let depth = 0;
|
|
10176
|
+
let inQuote = false;
|
|
10177
|
+
let tupleStart = -1;
|
|
10178
|
+
for (let i = 0; i < tuplesText.length; i++) {
|
|
10179
|
+
const ch = tuplesText[i];
|
|
10180
|
+
const next = i + 1 < tuplesText.length ? tuplesText[i + 1] : void 0;
|
|
10181
|
+
if (ch === "'") {
|
|
10182
|
+
if (inQuote && next === "'") {
|
|
10183
|
+
i += 1;
|
|
10184
|
+
continue;
|
|
10185
|
+
}
|
|
10186
|
+
inQuote = !inQuote;
|
|
10187
|
+
}
|
|
10188
|
+
if (inQuote) continue;
|
|
10189
|
+
if (ch === "(") {
|
|
10190
|
+
if (depth === 0) tupleStart = i + 1;
|
|
10191
|
+
depth += 1;
|
|
10192
|
+
} else if (ch === ")") {
|
|
10193
|
+
depth -= 1;
|
|
10194
|
+
if (depth === 0 && tupleStart >= 0) {
|
|
10195
|
+
tuples.push(tuplesText.slice(tupleStart, i));
|
|
10196
|
+
tupleStart = -1;
|
|
10197
|
+
}
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
const rows = seed[tableName] ?? [];
|
|
10201
|
+
let nextId = nextIds.get(tableName) ?? 1;
|
|
10202
|
+
for (const tuple of tuples) {
|
|
10203
|
+
const rawValues = splitSqlTopLevel(tuple, ",");
|
|
10204
|
+
const row = {};
|
|
10205
|
+
for (let i = 0; i < columns.length; i++) {
|
|
10206
|
+
const column = columns[i];
|
|
10207
|
+
row[column] = parseSqlLiteral(rawValues[i] ?? "null");
|
|
10208
|
+
}
|
|
10209
|
+
if (tablesWithNumericId.has(tableName)) {
|
|
10210
|
+
if (typeof row["id"] === "number") {
|
|
10211
|
+
nextId = Math.max(nextId, row["id"] + 1);
|
|
10212
|
+
} else if (typeof row["id"] === "string" && /^-?\d+$/.test(row["id"])) {
|
|
10213
|
+
const parsed = Number(row["id"]);
|
|
10214
|
+
row["id"] = parsed;
|
|
10215
|
+
nextId = Math.max(nextId, parsed + 1);
|
|
10216
|
+
} else {
|
|
10217
|
+
row["id"] = nextId;
|
|
10218
|
+
nextId += 1;
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
rows.push(row);
|
|
10222
|
+
}
|
|
10223
|
+
nextIds.set(tableName, nextId);
|
|
10224
|
+
seed[tableName] = rows;
|
|
10225
|
+
}
|
|
10226
|
+
return seed;
|
|
10227
|
+
}
|
|
10228
|
+
function loadSeedStateFromPath(seedRoot, seedName) {
|
|
10229
|
+
const jsonPath = resolve5(seedRoot, `${seedName}.json`);
|
|
10230
|
+
if (existsSync11(jsonPath)) {
|
|
10231
|
+
return JSON.parse(readFileSync13(jsonPath, "utf-8"));
|
|
10232
|
+
}
|
|
10233
|
+
const sqlPath = resolve5(seedRoot, `${seedName}.sql`);
|
|
10234
|
+
if (existsSync11(sqlPath)) {
|
|
10235
|
+
return parseSqlSeed(readFileSync13(sqlPath, "utf-8"));
|
|
10236
|
+
}
|
|
10237
|
+
return null;
|
|
10238
|
+
}
|
|
9947
10239
|
function loadBaseSeedFromDisk(twinName, seedName) {
|
|
9948
10240
|
const __dir = dirname3(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
9949
|
-
const
|
|
9950
|
-
|
|
9951
|
-
|
|
10241
|
+
const monorepoSeedRoots = [
|
|
10242
|
+
resolve5(__dir, "..", "..", "twins", twinName, "seeds"),
|
|
10243
|
+
resolve5(__dir, "..", "..", "..", "twins", twinName, "seeds")
|
|
10244
|
+
];
|
|
10245
|
+
for (const monorepoSeedRoot of monorepoSeedRoots) {
|
|
10246
|
+
const monorepoSeed = loadSeedStateFromPath(monorepoSeedRoot, seedName);
|
|
10247
|
+
if (monorepoSeed) {
|
|
10248
|
+
return monorepoSeed;
|
|
10249
|
+
}
|
|
9952
10250
|
}
|
|
9953
10251
|
try {
|
|
9954
10252
|
const req = createRequire2(import.meta.url);
|
|
9955
10253
|
const twinMain = req.resolve(`@archal/twin-${twinName}`);
|
|
9956
|
-
const
|
|
9957
|
-
|
|
9958
|
-
|
|
10254
|
+
const seedRoot = resolve5(dirname3(twinMain), "..", "seeds");
|
|
10255
|
+
const seedState = loadSeedStateFromPath(seedRoot, seedName);
|
|
10256
|
+
if (seedState) {
|
|
10257
|
+
return seedState;
|
|
9959
10258
|
}
|
|
9960
10259
|
} catch {
|
|
9961
10260
|
}
|
|
@@ -10422,34 +10721,27 @@ Run 'archal doctor' for a full system check.`
|
|
|
10422
10721
|
successCriteria: scenario.successCriteria.map((criterion) => `${criterion.type}: ${criterion.description}`)
|
|
10423
10722
|
};
|
|
10424
10723
|
for (const sel of seedSelections) {
|
|
10425
|
-
if (!options.allowAmbiguousSeed) {
|
|
10426
|
-
if (!options.noSeedCache) {
|
|
10427
|
-
const negative = getNegativeSeed(sel.twinName, sel.seedName, scenario.setup, { cacheContext: seedPromptContext });
|
|
10428
|
-
if (negative && negative.missingSlots.length > 0) {
|
|
10429
|
-
const details2 = formatMissingSlots(negative.missingSlots);
|
|
10430
|
-
throw new Error(
|
|
10431
|
-
`Setup is ambiguous for twin "${sel.twinName}" and cannot safely generate a dynamic seed.
|
|
10432
|
-
Missing details:
|
|
10433
|
-
${details2}
|
|
10434
|
-
Pass --allow-ambiguous-seed to opt into best-effort generation.`
|
|
10435
|
-
);
|
|
10436
|
-
}
|
|
10437
|
-
}
|
|
10438
|
-
}
|
|
10439
10724
|
const intentResult = extractSeedIntent(sel.twinName, scenario.setup);
|
|
10440
10725
|
extractedIntentByTwin.set(sel.twinName, intentResult.intent ?? void 0);
|
|
10441
10726
|
if (intentResult.missingSlots.length === 0) {
|
|
10442
10727
|
generationTargets.push(sel);
|
|
10443
10728
|
continue;
|
|
10444
10729
|
}
|
|
10445
|
-
|
|
10730
|
+
let missingSlots = intentResult.missingSlots;
|
|
10731
|
+
if (!options.noSeedCache) {
|
|
10732
|
+
const negative = getNegativeSeed(sel.twinName, sel.seedName, scenario.setup, { cacheContext: seedPromptContext });
|
|
10733
|
+
if (negative && negative.missingSlots.length > 0) {
|
|
10734
|
+
missingSlots = negative.missingSlots;
|
|
10735
|
+
}
|
|
10736
|
+
}
|
|
10737
|
+
const details = formatMissingSlots(missingSlots);
|
|
10446
10738
|
const message = `Setup is ambiguous for twin "${sel.twinName}" and cannot safely generate a dynamic seed.
|
|
10447
10739
|
Missing details:
|
|
10448
10740
|
${details}
|
|
10449
10741
|
Pass --allow-ambiguous-seed to opt into best-effort generation.`;
|
|
10450
10742
|
if (!options.allowAmbiguousSeed) {
|
|
10451
10743
|
if (!options.noSeedCache) {
|
|
10452
|
-
cacheNegativeSeed(sel.twinName, sel.seedName, scenario.setup,
|
|
10744
|
+
cacheNegativeSeed(sel.twinName, sel.seedName, scenario.setup, missingSlots, {
|
|
10453
10745
|
cacheContext: seedPromptContext
|
|
10454
10746
|
});
|
|
10455
10747
|
}
|
|
@@ -10471,7 +10763,7 @@ Pass --allow-ambiguous-seed to opt into best-effort generation.`;
|
|
|
10471
10763
|
const baseSeedData = loadBaseSeedFromDisk(sel.twinName, sel.seedName);
|
|
10472
10764
|
if (!baseSeedData || Object.keys(baseSeedData).length === 0) {
|
|
10473
10765
|
throw new Error(
|
|
10474
|
-
`Could not load base seed "${sel.seedName}" for twin "${sel.twinName}" from disk. Ensure the seed file exists at twins/${sel.twinName}/seeds/${sel.seedName}.json`
|
|
10766
|
+
`Could not load base seed "${sel.seedName}" for twin "${sel.twinName}" from disk. Ensure the seed file exists at twins/${sel.twinName}/seeds/${sel.seedName}.json or .sql`
|
|
10475
10767
|
);
|
|
10476
10768
|
}
|
|
10477
10769
|
progress(`Generating dynamic seed for ${sel.twinName}...`);
|
|
@@ -11495,23 +11787,6 @@ function createRunCommand() {
|
|
|
11495
11787
|
process.env["ARCHAL_ENGINE_API_KEY"] = userConfig.engineApiKey;
|
|
11496
11788
|
}
|
|
11497
11789
|
}
|
|
11498
|
-
if (!process.env["ARCHAL_ENGINE_API_KEY"]) {
|
|
11499
|
-
const providerEnvVars = [
|
|
11500
|
-
{ env: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash" },
|
|
11501
|
-
{ env: "OPENAI_API_KEY", defaultModel: "gpt-4o" },
|
|
11502
|
-
{ env: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514" }
|
|
11503
|
-
];
|
|
11504
|
-
for (const { env, defaultModel } of providerEnvVars) {
|
|
11505
|
-
const val = process.env[env]?.trim();
|
|
11506
|
-
if (val) {
|
|
11507
|
-
process.env["ARCHAL_ENGINE_API_KEY"] = val;
|
|
11508
|
-
if (!opts.engineModel && !process.env["ARCHAL_ENGINE_MODEL"]) {
|
|
11509
|
-
opts.engineModel = defaultModel;
|
|
11510
|
-
}
|
|
11511
|
-
break;
|
|
11512
|
-
}
|
|
11513
|
-
}
|
|
11514
|
-
}
|
|
11515
11790
|
let engine;
|
|
11516
11791
|
try {
|
|
11517
11792
|
engine = resolveEngineConfig(opts, timeout);
|
|
@@ -11527,6 +11802,37 @@ function createRunCommand() {
|
|
|
11527
11802
|
`
|
|
11528
11803
|
);
|
|
11529
11804
|
}
|
|
11805
|
+
if (engine.mode === "local" && !process.env["ARCHAL_ENGINE_API_KEY"]) {
|
|
11806
|
+
const explicitModel = firstNonEmpty(
|
|
11807
|
+
opts.engineModel,
|
|
11808
|
+
process.env["ARCHAL_ENGINE_MODEL"],
|
|
11809
|
+
resolveOpenClawModel(firstNonEmpty(opts.openclawAgent, process.env["OPENCLAW_AGENT_ID"]))
|
|
11810
|
+
);
|
|
11811
|
+
if (explicitModel) {
|
|
11812
|
+
const provider = detectProvider(explicitModel);
|
|
11813
|
+
const envVar = getProviderEnvVar(provider);
|
|
11814
|
+
const providerKey = process.env[envVar]?.trim();
|
|
11815
|
+
if (providerKey) {
|
|
11816
|
+
process.env["ARCHAL_ENGINE_API_KEY"] = providerKey;
|
|
11817
|
+
}
|
|
11818
|
+
} else {
|
|
11819
|
+
const providerEnvVars = [
|
|
11820
|
+
{ env: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash" },
|
|
11821
|
+
{ env: "OPENAI_API_KEY", defaultModel: "gpt-4o" },
|
|
11822
|
+
{ env: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514" }
|
|
11823
|
+
];
|
|
11824
|
+
for (const { env, defaultModel } of providerEnvVars) {
|
|
11825
|
+
const val = process.env[env]?.trim();
|
|
11826
|
+
if (val) {
|
|
11827
|
+
process.env["ARCHAL_ENGINE_API_KEY"] = val;
|
|
11828
|
+
if (!opts.engineModel && !process.env["ARCHAL_ENGINE_MODEL"]) {
|
|
11829
|
+
opts.engineModel = defaultModel;
|
|
11830
|
+
}
|
|
11831
|
+
break;
|
|
11832
|
+
}
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
}
|
|
11530
11836
|
if (engine.mode === "local" && !process.env["ARCHAL_ENGINE_API_KEY"]) {
|
|
11531
11837
|
process.stderr.write(
|
|
11532
11838
|
"Error: No API key found. The agent harness needs an API key to call the model.\nSet one of:\n GEMINI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY env var\n archal config set engine.apiKey <key>\n ARCHAL_ENGINE_API_KEY env var\n"
|
|
@@ -11591,11 +11897,19 @@ function createRunCommand() {
|
|
|
11591
11897
|
);
|
|
11592
11898
|
}
|
|
11593
11899
|
if (!runFailureMessage) {
|
|
11594
|
-
const
|
|
11900
|
+
const configuredReadyTimeoutMs = (() => {
|
|
11901
|
+
const raw = process.env["ARCHAL_SESSION_READY_TIMEOUT_MS"]?.trim();
|
|
11902
|
+
if (!raw) return 3e5;
|
|
11903
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11904
|
+
return Number.isNaN(parsed) || parsed <= 0 ? 3e5 : parsed;
|
|
11905
|
+
})();
|
|
11906
|
+
const SESSION_READY_TIMEOUT_MS = Math.max(12e4, configuredReadyTimeoutMs);
|
|
11595
11907
|
const SESSION_POLL_INTERVAL_MS = 3e3;
|
|
11908
|
+
const STATUS_READY_GRACE_MS = 15e3;
|
|
11596
11909
|
const readyDeadline = Date.now() + SESSION_READY_TIMEOUT_MS;
|
|
11597
11910
|
let sessionReady = false;
|
|
11598
11911
|
let lastPollIssue;
|
|
11912
|
+
let statusReadySinceMs = null;
|
|
11599
11913
|
const isRetryablePollFailure = (result) => result.offline || typeof result.status === "number" && result.status >= 500;
|
|
11600
11914
|
const sleepForPollInterval = async () => new Promise((resolve13) => setTimeout(resolve13, SESSION_POLL_INTERVAL_MS));
|
|
11601
11915
|
while (Date.now() < readyDeadline) {
|
|
@@ -11642,11 +11956,26 @@ function createRunCommand() {
|
|
|
11642
11956
|
break;
|
|
11643
11957
|
}
|
|
11644
11958
|
const healthAlive = healthResult.ok && healthResult.data.alive;
|
|
11645
|
-
const statusAlive = statusResult.data.alive ||
|
|
11959
|
+
const statusAlive = statusResult.data.alive || status === "ready";
|
|
11646
11960
|
if (statusAlive && healthAlive) {
|
|
11647
11961
|
sessionReady = true;
|
|
11648
11962
|
break;
|
|
11649
11963
|
}
|
|
11964
|
+
if (statusAlive && !healthAlive) {
|
|
11965
|
+
if (statusReadySinceMs === null) {
|
|
11966
|
+
statusReadySinceMs = Date.now();
|
|
11967
|
+
}
|
|
11968
|
+
const readyForMs = Date.now() - statusReadySinceMs;
|
|
11969
|
+
if (readyForMs >= STATUS_READY_GRACE_MS) {
|
|
11970
|
+
warn(
|
|
11971
|
+
`Session ${backendSessionId} reported status=ready while health endpoint remained starting for ${readyForMs}ms; proceeding.`
|
|
11972
|
+
);
|
|
11973
|
+
sessionReady = true;
|
|
11974
|
+
break;
|
|
11975
|
+
}
|
|
11976
|
+
} else {
|
|
11977
|
+
statusReadySinceMs = null;
|
|
11978
|
+
}
|
|
11650
11979
|
lastPollIssue = `session still starting (status=${status}, health=${healthAlive ? "alive" : "starting"})`;
|
|
11651
11980
|
await sleepForPollInterval();
|
|
11652
11981
|
}
|