elanous 0.2.15 → 0.2.16
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +36 -2
- package/src/harness/harness-pod-dispatch.ts +25 -55
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/hq/hq.ts +8 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +112 -6
- package/src/intake-plane/digest.ts +48 -26
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +90 -3
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/goal-author.ts +19 -13
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/orchestrator.ts +138 -7
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +36 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -364,7 +364,61 @@ export function decideProviderFallback(input: {
|
|
|
364
364
|
return { action: 'advance', nextProvider, category, attempts };
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
/** Consecutive overload/5xx/rate-limit outcomes that switch the child off codex
|
|
368
|
+
* before the same provider spends its remaining retries. Other errors (400)
|
|
369
|
+
* never count. Recovery is this many later non-overload outcomes. */
|
|
370
|
+
export const OVERLOAD_FAILOVER_STREAK = 3;
|
|
371
|
+
|
|
372
|
+
export type LlmCallOutcomeKind = 'ok' | 'overloaded' | '5xx' | 'rate-limit' | 'other';
|
|
373
|
+
|
|
374
|
+
/** One finished LLM call. Launch failover reads only this shape. */
|
|
375
|
+
export interface LlmCallOutcome {
|
|
376
|
+
readonly provider: string;
|
|
377
|
+
readonly status: number;
|
|
378
|
+
readonly kind: LlmCallOutcomeKind;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const CODEX_CALL_PROVIDERS = new Set(['codex', 'openai-codex']);
|
|
382
|
+
|
|
383
|
+
export function llmCallOutcomeKind(status: number, message = ''): LlmCallOutcomeKind {
|
|
384
|
+
if (status >= 200 && status < 300) return 'ok';
|
|
385
|
+
const msg = message.toLowerCase();
|
|
386
|
+
if (status === 429 || msg.includes('rate limit') || msg.includes('too many requests')) return 'rate-limit';
|
|
387
|
+
if (
|
|
388
|
+
status === 500 || status === 502 || status === 503 || status === 504 ||
|
|
389
|
+
msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
|
|
390
|
+
) {
|
|
391
|
+
return status >= 500 ? '5xx' : 'overloaded';
|
|
392
|
+
}
|
|
393
|
+
if (msg.includes('overloaded')) return 'overloaded';
|
|
394
|
+
return 'other';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function isOverloadOutcomeKind(kind: LlmCallOutcomeKind): boolean {
|
|
398
|
+
return kind === 'overloaded' || kind === '5xx' || kind === 'rate-limit';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** True when the newest `limit` codex outcomes are all overload-class.
|
|
402
|
+
* A non-codex row or a non-overload row breaks the streak. Empty input is false. */
|
|
403
|
+
export function codexOverloadStreakReached(
|
|
404
|
+
outcomes: readonly LlmCallOutcome[],
|
|
405
|
+
limit = OVERLOAD_FAILOVER_STREAK,
|
|
406
|
+
): boolean {
|
|
407
|
+
if (limit < 1 || outcomes.length < limit) return false;
|
|
408
|
+
const tail = outcomes.slice(-limit);
|
|
409
|
+
return tail.every((row) => CODEX_CALL_PROVIDERS.has(row.provider) && isOverloadOutcomeKind(row.kind));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** True when the newest `limit` outcomes are successful (any provider). */
|
|
413
|
+
export function overloadRecovered(
|
|
414
|
+
outcomes: readonly LlmCallOutcome[],
|
|
415
|
+
limit = OVERLOAD_FAILOVER_STREAK,
|
|
416
|
+
): boolean {
|
|
417
|
+
if (limit < 1 || outcomes.length < limit) return false;
|
|
418
|
+
return outcomes.slice(-limit).every((row) => row.kind === 'ok');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export type RetryAction = 'retry' | 'abort' | 'ask-user' | 'auto-undo' | 'switch-provider';
|
|
368
422
|
|
|
369
423
|
export interface RetryDecision {
|
|
370
424
|
action: RetryAction;
|
|
@@ -417,6 +471,16 @@ export interface RetryContext {
|
|
|
417
471
|
/** Optional: retry-after header string from a 429 response (ms or
|
|
418
472
|
* seconds or HTTP date). decideRetry parses it. */
|
|
419
473
|
retryAfter?: string;
|
|
474
|
+
/** Configured fallbacks still unused. When > 0, an overload-class
|
|
475
|
+
* failure that has already repeated `overloadStreak` times switches
|
|
476
|
+
* provider instead of spending another retry on the same one.
|
|
477
|
+
* Omitted or 0 keeps the historical retry-until-exhausted behaviour. */
|
|
478
|
+
remainingFallbacks?: number;
|
|
479
|
+
/** Consecutive overload-class failures already seen for this call,
|
|
480
|
+
* including the one under decision. Default threshold is
|
|
481
|
+
* OVERLOAD_FAILOVER_STREAK. */
|
|
482
|
+
overloadStreak?: number;
|
|
483
|
+
overloadStreakLimit?: number;
|
|
420
484
|
}
|
|
421
485
|
|
|
422
486
|
/** Classify an error into a RetryCategory without making an action
|
|
@@ -519,10 +583,42 @@ export function classifyError(err: unknown): RetryCategory {
|
|
|
519
583
|
return 'unknown';
|
|
520
584
|
}
|
|
521
585
|
|
|
586
|
+
function isOverloadSwitchCandidate(msg: string, code: string | null): boolean {
|
|
587
|
+
return (
|
|
588
|
+
code === '429' || code === '500' || code === '502' || code === '503' || code === '504' ||
|
|
589
|
+
msg.includes('rate limit') || msg.includes('too many requests') ||
|
|
590
|
+
msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function overloadSwitchDecision(ctx: RetryContext): RetryDecision | undefined {
|
|
595
|
+
const remaining = ctx.remainingFallbacks ?? 0;
|
|
596
|
+
if (remaining <= 0) return undefined;
|
|
597
|
+
const limit = ctx.overloadStreakLimit ?? OVERLOAD_FAILOVER_STREAK;
|
|
598
|
+
const streak = ctx.overloadStreak ?? 1;
|
|
599
|
+
if (streak < limit) return undefined;
|
|
600
|
+
return {
|
|
601
|
+
action: 'switch-provider',
|
|
602
|
+
category: 'overloaded',
|
|
603
|
+
reason: `overload streak ${streak} with ${remaining} fallback(s) left; switch before spending retries`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
522
607
|
/** Pure verdict. Caller sleeps + retries or aborts per the returned
|
|
523
608
|
* action. Exponential backoff baseline: 500ms * 2^attempt, clamped
|
|
524
609
|
* to [500, 30_000]. */
|
|
525
610
|
export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
|
|
611
|
+
const msg = extractMessage(err).toLowerCase();
|
|
612
|
+
const code = extractErrorCode(err);
|
|
613
|
+
|
|
614
|
+
// Overload with a fallback still unused switches before the doom
|
|
615
|
+
// window (k === 3) turns the same 503 into auto-undo. A 400 is not
|
|
616
|
+
// a candidate. Doom still wins for every other repeated failure.
|
|
617
|
+
if (isOverloadSwitchCandidate(msg, code)) {
|
|
618
|
+
const switched = overloadSwitchDecision(ctx);
|
|
619
|
+
if (switched) return switched;
|
|
620
|
+
}
|
|
621
|
+
|
|
526
622
|
// Doom-loop always wins — even a technically-retryable error
|
|
527
623
|
// becomes auto-undo after N identical failures. The caller may
|
|
528
624
|
// still degrade to ask-user when plan mode is active or when the
|
|
@@ -535,9 +631,6 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
|
|
|
535
631
|
};
|
|
536
632
|
}
|
|
537
633
|
|
|
538
|
-
const msg = extractMessage(err).toLowerCase();
|
|
539
|
-
const code = extractErrorCode(err);
|
|
540
|
-
|
|
541
634
|
// Context window exhausted — never retry (the next call will also
|
|
542
635
|
// be over-sized). Abort and surface the error to the caller; upstream
|
|
543
636
|
// can trigger compaction.
|
|
@@ -589,8 +682,16 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
|
|
|
589
682
|
};
|
|
590
683
|
}
|
|
591
684
|
|
|
592
|
-
// Overloaded / server busy.
|
|
593
|
-
if (
|
|
685
|
+
// Overloaded / server busy / 5xx.
|
|
686
|
+
if (
|
|
687
|
+
msg.includes('overloaded') ||
|
|
688
|
+
msg.includes('server_busy') ||
|
|
689
|
+
msg.includes('server_error') ||
|
|
690
|
+
code === '500' ||
|
|
691
|
+
code === '502' ||
|
|
692
|
+
code === '503' ||
|
|
693
|
+
code === '504'
|
|
694
|
+
) {
|
|
594
695
|
return {
|
|
595
696
|
action: 'retry',
|
|
596
697
|
delayMs: backoffMs(ctx.attempt),
|
|
@@ -57,7 +57,22 @@ export async function recordWorkingBackwardsOnCards(
|
|
|
57
57
|
debug.log('steward.working-backwards', 'skipped-launched-before-draft', { issueId: issue.identifier });
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
// An invalid draft for the same issue text is not re-asked every tick — each ask is a 70-100 s LLM call (10-05 ELA-29 timed the schedule stage out 3×).
|
|
61
|
+
const invalidKey = `hitl:draft-invalid-${createHash('sha256').update(JSON.stringify({ title: issue.title, body: issue.body ?? '' })).digest('hex').slice(0, 16)}`;
|
|
62
|
+
if (card.sections.some(section => section.key === invalidKey)) {
|
|
63
|
+
debug.log('steward.working-backwards', 'skipped-known-invalid', { issueId: issue.identifier });
|
|
64
|
+
throw new InvalidWorkingBackwardsDraft();
|
|
65
|
+
}
|
|
66
|
+
let draft: WorkingBackwardsDraft;
|
|
67
|
+
try { draft = await draftWorkingBackwards(issue, ask); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof InvalidWorkingBackwardsDraft) {
|
|
70
|
+
// The memo is best-effort — failing to write it must not replace the invalid-draft error the caller routes to hitl.
|
|
71
|
+
try { store.appendSection(card.id, { key: invalidKey, owner: 'steward', content: 'working-backwards draft invalid for this issue text — not re-asked until the issue changes' }); }
|
|
72
|
+
catch (memoError) { debug.log('steward.working-backwards', 'invalid-memo-failed', { issueId: issue.identifier, error: memoError instanceof Error ? memoError.message : String(memoError) }); }
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
61
76
|
for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
|
|
62
77
|
const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
|
|
63
78
|
if (!card.sections.some(item => item.key === key)) store.appendSection(card.id, { key, owner: 'steward', content });
|
|
@@ -46,6 +46,31 @@ export function registerCardCommand(program: Command, deps: CardCliDeps = {}): C
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
card.command('close <id>')
|
|
50
|
+
.description('Close a task card with a reason (owner operation)')
|
|
51
|
+
.requiredOption('--reason <text>', 'One-line close reason')
|
|
52
|
+
.action((id: string, options: { reason: string }) => {
|
|
53
|
+
const store = createStore();
|
|
54
|
+
try {
|
|
55
|
+
const reason = options.reason.trim();
|
|
56
|
+
if (!reason || /[\r\n]/.test(options.reason)) throw new Error('Close reason must be one line');
|
|
57
|
+
const existing = store.getCard(id);
|
|
58
|
+
if (!existing) {
|
|
59
|
+
process.stderr.write(`Card not found: ${id}\n`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (existing.status === 'closed') {
|
|
64
|
+
write(`이미 닫힘: ${id}\n`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
store.closeCard(id, reason);
|
|
68
|
+
write(`닫힘: ${id}\n`);
|
|
69
|
+
} finally {
|
|
70
|
+
store.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
49
74
|
card.command('list')
|
|
50
75
|
.description('List task cards')
|
|
51
76
|
.option('--open', 'Only show open cards')
|
|
@@ -32,7 +32,7 @@ export interface AppendSectionInput {
|
|
|
32
32
|
type CardEvent =
|
|
33
33
|
| { type: 'created'; id: string; goalId: string; title: string; createdAt: string }
|
|
34
34
|
| { type: 'section'; key: string; owner: string; content: string; createdAt: string }
|
|
35
|
-
| { type: 'closed'; createdAt: string };
|
|
35
|
+
| { type: 'closed'; createdAt: string; reason?: string };
|
|
36
36
|
|
|
37
37
|
/** RFC §3 (내부 문서 `RFC-execution-and-landing-loop-agents-parallel-first-2026-09-29`) — each card section has one owner.
|
|
38
38
|
* A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
|
|
@@ -197,13 +197,15 @@ export class CardStore {
|
|
|
197
197
|
}).immediate();
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
closeCard(id: string): TaskCard {
|
|
200
|
+
closeCard(id: string, reason?: string): TaskCard {
|
|
201
|
+
if (reason !== undefined && !reason.trim()) throw new Error('Close reason is required');
|
|
201
202
|
return this.db.transaction(() => {
|
|
202
203
|
this.reconcile();
|
|
203
204
|
const card = this.readCard(id);
|
|
204
205
|
if (!card) throw new Error(`Card not found: ${id}`);
|
|
205
206
|
if (card.status === 'closed') return card;
|
|
206
|
-
this.append(id, { type: 'closed', createdAt: new Date().toISOString()
|
|
207
|
+
this.append(id, { type: 'closed', createdAt: new Date().toISOString(),
|
|
208
|
+
...(reason === undefined ? {} : { reason: redactSecrets(reason.trim()) }) });
|
|
207
209
|
return { ...card, status: 'closed' as const };
|
|
208
210
|
}).immediate();
|
|
209
211
|
}
|
|
@@ -3,6 +3,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
|
|
5
5
|
import { ledgerLineToLogEvent } from './pod-ledger-events.js';
|
|
6
|
+
import { defaultPodReemit } from './pod-ledger-prod-sink.js';
|
|
6
7
|
|
|
7
8
|
type Ledger = { runId: string; jsonl: string };
|
|
8
9
|
type Incomplete = { runId: string; reason: string };
|
|
@@ -94,7 +95,7 @@ export function parsePodLedgerChunks(logs: string): ParseResult {
|
|
|
94
95
|
* except a ledger that THIS run's live follower created (`replace`): that one is a partial copy and the final is complete. */
|
|
95
96
|
export function collectPodLedgers(
|
|
96
97
|
logs: string,
|
|
97
|
-
{ dir = runLedgerDir(), log = (
|
|
98
|
+
{ dir = runLedgerDir(), log = defaultPodReemit(), emit = defaultPodReemit(), replace = new Set<string>() }: {
|
|
98
99
|
dir?: string;
|
|
99
100
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
100
101
|
emit?: Emit;
|
|
@@ -146,8 +147,8 @@ export function createPodLedgerFollower(opts: {
|
|
|
146
147
|
onStall?: (message: string) => void;
|
|
147
148
|
}): { poll(): void; readonly owned: boolean } {
|
|
148
149
|
const dir = opts.dir ?? runLedgerDir();
|
|
149
|
-
const log = opts.log ?? (
|
|
150
|
-
const emit: Emit = opts.emit ?? (
|
|
150
|
+
const log = opts.log ?? defaultPodReemit();
|
|
151
|
+
const emit: Emit = opts.emit ?? defaultPodReemit();
|
|
151
152
|
const now = opts.now ?? Date.now;
|
|
152
153
|
const stallMinutes = opts.stallMinutes ?? 30;
|
|
153
154
|
if (!Number.isFinite(stallMinutes) || stallMinutes <= 0) throw new RangeError('stallMinutes must be positive');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { debug } from '../../debug/log.js';
|
|
2
|
+
import { LogStore, logsDbPath } from '../../mss/logging/log-store.js';
|
|
3
|
+
|
|
4
|
+
type Emit = (category: string, event: string, data: Record<string, unknown>) => void;
|
|
5
|
+
type Store = Pick<LogStore, 'insertBatch'>;
|
|
6
|
+
|
|
7
|
+
/** 발사한 부모(`harness say --config-dir …`)가 정한 집의 logs.db — 부모가 자식 `self orchestrate` 에 env 로 넘긴다. */
|
|
8
|
+
export const POD_REEMIT_LOGS_DB_ENV = 'ELANOUS_POD_REEMIT_LOGS_DB';
|
|
9
|
+
export function launchHomeLogsDb(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
10
|
+
return env[POD_REEMIT_LOGS_DB_ENV]?.trim() || undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let cached: { path: string; store: Store } | null = null;
|
|
14
|
+
let announced = '';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* POD-OBS(10-06 · 0.2.16 P0 · OP 사양): Pod 자식 원장 «재방출»만 부모가 발사 때 정한 집(A)의 logs.db 로 보낸다.
|
|
18
|
+
* 자식 orchestrate 는 트리 파생 test 우주(B)에서 돌아, 지금은 재방출이 B 에만 쌓였다(24h `ledger-collected` 운영 2 vs 다른 우주 250).
|
|
19
|
+
* 자식 우주 원장·격리는 그대로 둔다. A 를 못 열면 조용히 버리지 않고 B 로 떨어뜨리며 경고한다.
|
|
20
|
+
* env 가 없거나(옛 발사) A == B 이거나 시험 프로세스면 null → 호출측이 지금처럼 B 에만 쓴다.
|
|
21
|
+
*/
|
|
22
|
+
export function operationalReemit(opts: {
|
|
23
|
+
currentDb?: string;
|
|
24
|
+
operationalDb?: string;
|
|
25
|
+
open?: (path: string) => Store;
|
|
26
|
+
testProcess?: boolean;
|
|
27
|
+
fallback?: Emit;
|
|
28
|
+
} = {}): Emit | null {
|
|
29
|
+
const operationalDb = opts.operationalDb ?? launchHomeLogsDb();
|
|
30
|
+
if (!operationalDb) return null;
|
|
31
|
+
const currentDb = opts.currentDb ?? logsDbPath();
|
|
32
|
+
const testProcess = opts.testProcess ?? (process.env.NODE_ENV === 'test' || !!process.env.ELANOUS_TEST_HOME);
|
|
33
|
+
if (testProcess && !opts.open) return null;
|
|
34
|
+
if (currentDb === operationalDb) return null;
|
|
35
|
+
const open = opts.open ?? ((path: string) => new LogStore(path, { instance: 'prod' }));
|
|
36
|
+
const fallback: Emit = opts.fallback ?? ((c, e, d) => debug.log(c, e, d));
|
|
37
|
+
const write = (category: string, event: string, data: Record<string, unknown>): void => {
|
|
38
|
+
if (!cached || cached.path !== operationalDb || opts.open) cached = { path: operationalDb, store: open(operationalDb) };
|
|
39
|
+
cached.store.insertBatch([{ rec: { ts: new Date().toISOString(), category, event, data }, surface: 'pod-reemit' }]);
|
|
40
|
+
};
|
|
41
|
+
let warned = false;
|
|
42
|
+
return (category, event, data) => {
|
|
43
|
+
try {
|
|
44
|
+
if (announced !== `${currentDb}->${operationalDb}` || opts.open) {
|
|
45
|
+
// 어느 우주로 보냈는지 한 줄(A 에) — 조회자가 «B 에 왜 없나»를 여기서 본다.
|
|
46
|
+
write('self-implement.pod', 'reemit-target', { targetUniverse: operationalDb, childUniverse: currentDb });
|
|
47
|
+
announced = `${currentDb}->${operationalDb}`;
|
|
48
|
+
}
|
|
49
|
+
write(category, event, data);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
fallback(category, event, data);
|
|
52
|
+
if (warned) return;
|
|
53
|
+
// 경고는 싱크(발사 하나)당 한 번 — 줄마다 찍으면 경고가 원장을 덮는다.
|
|
54
|
+
warned = true;
|
|
55
|
+
try {
|
|
56
|
+
fallback('self-implement.pod', 'reemit-target-unavailable', {
|
|
57
|
+
targetUniverse: operationalDb, childUniverse: currentDb, reason: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
} catch { /* nothing */ }
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let shared: { key: string; emit: Emit } | null = null;
|
|
65
|
+
|
|
66
|
+
/** 재방출 기본 경로: 집(A)이 정해져 있으면 A 에만, 아니면 지금처럼 지금 우주(B)의 debug.log.
|
|
67
|
+
* 프로세스(발사)당 싱크 하나 — collector 의 `log`·`emit`, follower 가 같은 싱크를 써서 경고·대상 줄이 한 번이다. */
|
|
68
|
+
export function defaultPodReemit(): Emit {
|
|
69
|
+
const key = `${launchHomeLogsDb() ?? ''}->${logsDbPath()}`;
|
|
70
|
+
if (shared?.key === key) return shared.emit;
|
|
71
|
+
const operational = operationalReemit();
|
|
72
|
+
const emit: Emit = operational ?? ((category, event, data) => { debug.log(category, event, data); });
|
|
73
|
+
shared = { key, emit };
|
|
74
|
+
return emit;
|
|
75
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - 컨텍스트 kubectl 컨텍스트 이름(이 맥의 kubeconfig)
|
|
9
9
|
* - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
|
|
10
10
|
* - 상한 이 노드에 동시에 둘 Job 수(기본 2)
|
|
11
|
-
* Job 마다 앞
|
|
11
|
+
* Job 마다 «빈 자리 비율»이 가장 큰 노드를 고른다(동률은 앞 순위 · 측정 실패 노드는 측정된 노드가 다 찼을 때만). 다 차면 자리가 날 때까지 기다린다.
|
|
12
12
|
* 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
|
|
13
13
|
* 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
|
|
14
14
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
@@ -271,7 +271,9 @@ export class PodPoolScheduler {
|
|
|
271
271
|
return this.admission.snapshot();
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
274
|
-
* Pick the member with the
|
|
274
|
+
* Pick the member with the largest free share: (capacity − measured cluster occupancy − this process's inflight) / capacity.
|
|
275
|
+
* POOL-SPREAD (10-06): comparing absolute free slots always picked the big member (node-b 25 vs node-c 4), so the small
|
|
276
|
+
* member idled until the big one was full. Comparing the free share fills members in proportion to their capacity.
|
|
275
277
|
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
276
278
|
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
277
279
|
*/
|
|
@@ -284,7 +286,12 @@ export class PodPoolScheduler {
|
|
|
284
286
|
}
|
|
285
287
|
const free: Record<string, number> = {};
|
|
286
288
|
const measured: Record<string, boolean> = {};
|
|
287
|
-
let best: { member: PodPoolMember; slots: number } | null = null;
|
|
289
|
+
let best: { member: PodPoolMember; slots: number; measured: boolean } | null = null;
|
|
290
|
+
// A measured member always beats an unmeasured one (its share would read 100% and draw every Job to a node
|
|
291
|
+
// that did not answer); unmeasured members compete only when no measured member has a free slot.
|
|
292
|
+
const better = (cand: { member: PodPoolMember; slots: number; measured: boolean }) => !best
|
|
293
|
+
|| (cand.measured !== best.measured ? cand.measured
|
|
294
|
+
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
288
295
|
for (const m of this.members) {
|
|
289
296
|
const inflight = this.inflight.get(m.context) ?? 0;
|
|
290
297
|
const row = reading[m.context];
|
|
@@ -294,11 +301,13 @@ export class PodPoolScheduler {
|
|
|
294
301
|
const slots = m.capacity - occupied - inflight;
|
|
295
302
|
free[m.context] = slots;
|
|
296
303
|
if (slots <= 0) continue;
|
|
297
|
-
|
|
304
|
+
const cand = { member: m, slots, measured: ok };
|
|
305
|
+
if (better(cand)) best = cand;
|
|
298
306
|
}
|
|
299
307
|
if (!best) return null;
|
|
300
308
|
this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
|
|
301
|
-
|
|
309
|
+
const share = Object.fromEntries(this.members.map((m) => [m.context, Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100]));
|
|
310
|
+
debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured, share });
|
|
302
311
|
return best.member;
|
|
303
312
|
}
|
|
304
313
|
release(member: PodPoolMember): void {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Pod Job 원천 단계 — default clone · 커밋 · PR · 호스트 번들.
|
|
2
2
|
* sha·PR·커밋은 형식 검사 전에 스크립트에 넣지 않는다(셸 주입 차단). */
|
|
3
3
|
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
|
|
4
6
|
export type PodSource =
|
|
5
|
-
| { kind: 'default' }
|
|
7
|
+
| { kind: 'default'; base?: string }
|
|
6
8
|
| { kind: 'commit'; sha: string }
|
|
7
9
|
| { kind: 'pr'; number: number }
|
|
8
10
|
| { kind: 'bundle'; bundlePath: string; sha256: string; sizeBytes: number; headCommit: string };
|
|
@@ -29,6 +31,27 @@ function assertSize(value: number): number {
|
|
|
29
31
|
return value;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/** Check the requested branch on origin before creating a Pod Job, not in the launched job. */
|
|
35
|
+
function assertRemoteBase(repoUrl: string, base: string): void {
|
|
36
|
+
if (base.startsWith('-') || base === 'HEAD') throw new Error(`invalid Pod base ref: ${base}`);
|
|
37
|
+
try {
|
|
38
|
+
execFileSync('git', ['check-ref-format', '--branch', base], { stdio: 'pipe' });
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`invalid Pod base ref: ${base}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const advertised = execFileSync('git', ['ls-remote', '--exit-code', '--heads', repoUrl, `refs/heads/${base}`], {
|
|
44
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
+
}).trim();
|
|
46
|
+
const [sha, ref] = advertised.split('\t');
|
|
47
|
+
if (!/^[0-9a-f]{40,64}$/.test(sha ?? '') || ref !== `refs/heads/${base}`) {
|
|
48
|
+
throw new Error('origin did not advertise the requested branch');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(`Pod base ref not found on origin: ${base}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/** Job 스크립트의 원천 단계. 성공 갈래는 `ELANOUS_POD_SOURCE <kind> <HEAD>` 로 끝난다. */
|
|
33
56
|
export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath = '/host-mirror'): string {
|
|
34
57
|
const quotedUrl = `'${repoUrl.replace(/'/g, `'\\''`)}'`;
|
|
@@ -48,6 +71,19 @@ export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath =
|
|
|
48
71
|
'cd repo || exit 5',
|
|
49
72
|
];
|
|
50
73
|
if (source.kind === 'default') {
|
|
74
|
+
if (source.base && source.base !== 'main') {
|
|
75
|
+
assertRemoteBase(repoUrl, source.base);
|
|
76
|
+
const base = source.base;
|
|
77
|
+
const quote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
78
|
+
const quotedTrackingRef = quote(`refs/remotes/origin/${base}`);
|
|
79
|
+
return [
|
|
80
|
+
...start,
|
|
81
|
+
`git fetch --depth 50 origin ${quote(`+refs/heads/${base}:refs/remotes/origin/${base}`)} || exit 5`,
|
|
82
|
+
`git checkout -q -B ${quote(base)} ${quotedTrackingRef} || exit 5`,
|
|
83
|
+
'head=$(git rev-parse HEAD)',
|
|
84
|
+
"printf 'ELANOUS_POD_SOURCE default %s\\n' \"$head\"",
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
51
87
|
return [
|
|
52
88
|
...start,
|
|
53
89
|
'if [ "$source_via" = mirror ]; then',
|
|
@@ -38,7 +38,10 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
|
38
38
|
/** 자식 컨테이너 «요청» — 🩸 2026-09-27: limits 만 두면 k8s 가 requests=limits(cpu 4)로 잡아, 32코어 노드에
|
|
39
39
|
* «자리 요청»이 88% 차서 잡이 Pending 인데 실사용은 14%였다(풀 25 자리 중 ~8 만 떴다). 상한(limits)은 그대로 두고
|
|
40
40
|
* 예약만 실사용에 맞춘다. */
|
|
41
|
-
|
|
41
|
+
// POD-DIET (10-06 03:47~05:18 · node-b goal Pods · 89 one-minute samples · 42 Pods): per-Pod observed max median 4.6Gi ·
|
|
42
|
+
// p75 6.2Gi · p90 11.7Gi — 23/42 went above the old 4Gi request, so the scheduler over-packed. 6Gi ≈ p75; limits stay.
|
|
43
|
+
// Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
|
|
44
|
+
export const POD_CHILD_REQUESTS = { cpu: '1', memory: '6Gi' } as const;
|
|
42
45
|
import type { PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
43
46
|
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER } from './pod-lease.js';
|
|
44
47
|
import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
|
|
@@ -63,6 +66,7 @@ import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-
|
|
|
63
66
|
import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
|
|
64
67
|
import { getUserConfig } from '../../user-config.js';
|
|
65
68
|
import { extractPodFailureReason } from './pod-failure-reason.js';
|
|
69
|
+
import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
|
|
66
70
|
|
|
67
71
|
export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
|
|
68
72
|
|
|
@@ -70,7 +74,9 @@ export type Kubectl = (args: readonly string[], input?: string) => { status: num
|
|
|
70
74
|
// still the credential source for gh and setup-git's helper. Install it privately
|
|
71
75
|
// on that failure; rename keeps concurrent readers from seeing a partial token.
|
|
72
76
|
const APP_GH_CONFIG_PATH = '$HOME/.config/gh';
|
|
73
|
-
|
|
77
|
+
// PODCRED2 (10-05) — GitHub App installation tokens now carry `.` and `-` (390 chars on node-b); the old
|
|
78
|
+
// `[A-Za-z0-9_]+` check rejected every real token, so any transient `gh auth login` failure became exit 7.
|
|
79
|
+
export const APP_GH_AUTH_SCRIPT = 'const fs=require("node:fs"),path=require("node:path"),crypto=require("node:crypto");const token=fs.readFileSync(0,"utf8").replace(/\\r?\\n$/,"");if(!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(token))process.exit(1);const dir=process.env.GH_CONFIG_DIR||path.join(process.env.HOME,".config/gh");fs.mkdirSync(dir,{recursive:true,mode:0o700});const dest=path.join(dir,"hosts.yml"),temp=path.join(dir,".hosts.yml."+crypto.randomUUID());try{fs.writeFileSync(temp,"github.com:\\n oauth_token: "+token+"\\n git_protocol: https\\n",{mode:0o600,flag:"wx"});fs.renameSync(temp,dest);fs.chmodSync(dest,0o600)}finally{try{fs.unlinkSync(temp)}catch{}}';
|
|
74
80
|
const APP_GH_AUTH_COMMAND = `bun -e '${APP_GH_AUTH_SCRIPT}'`;
|
|
75
81
|
const APP_GH_EXEC = `unset GH_TOKEN GITHUB_TOKEN; GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}" exec "$@"`;
|
|
76
82
|
|
|
@@ -594,14 +600,15 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
|
|
|
594
600
|
return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
|
|
595
601
|
}
|
|
596
602
|
|
|
597
|
-
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
|
|
603
|
+
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
|
|
598
604
|
const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
|
|
599
605
|
const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
|
|
600
|
-
const
|
|
606
|
+
const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
|
|
607
|
+
const askBaseIndex = delegated ? o.args.indexOf('--base') : -1;
|
|
601
608
|
const quotedAsk = (askBaseIndex >= 0 && o.args[askBaseIndex + 1]
|
|
602
609
|
? ` '--base' '${o.args[askBaseIndex + 1]!.replace(/'/g, `'\\''`)}'`
|
|
603
|
-
: '') + (
|
|
604
|
-
+ (
|
|
610
|
+
: '') + (delegated && o.args.includes('--merge-by-host') ? ' --merge-by-host' : delegated && o.args.includes('--open-pr') && !o.args.includes('--auto-merge') ? ' --no-auto-merge' : '')
|
|
611
|
+
+ (delegated && o.args.includes('--no-supervise') ? ' --no-supervise' : '');
|
|
605
612
|
const script = [
|
|
606
613
|
'set -u',
|
|
607
614
|
o.grokCredential === 'subscription'
|
|
@@ -609,7 +616,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
609
616
|
: o.grokCredential === 'api_key'
|
|
610
617
|
? 'mkdir -p ~/.grok && install -m 600 /creds/grok-api-key ~/.grok/api-key && export XAI_API_KEY="$(cat ~/.grok/api-key)" && export ELANOUS_LLM_PROVIDER=grok'
|
|
611
618
|
: o.codexAccounts ? podCodexAccountScript(o.codexAccounts) : `mkdir -p ~/.elanous ~/.codex && cp /creds/elanous-auth.json ~/.elanous/auth.json && cp /creds/codex-auth.json ~/.codex/auth.json && chmod 600 ~/.elanous/auth.json ~/.codex/auth.json\n${podQuotaPolicyExport()}`,
|
|
612
|
-
o.appCredential ? `unset GH_TOKEN GITHUB_TOKEN; export GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}"; mkdir -p "$GH_CONFIG_DIR" && chmod 700 "$GH_CONFIG_DIR" && { gh auth login --with-token < /creds/gh-token
|
|
619
|
+
o.appCredential ? `unset GH_TOKEN GITHUB_TOKEN; export GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}"; mkdir -p "$GH_CONFIG_DIR" && chmod 700 "$GH_CONFIG_DIR" && { (umask 077; gh auth login --with-token < /creds/gh-token >"$HOME/.gh-login.log" 2>&1) || ${APP_GH_AUTH_COMMAND} < /creds/gh-token; } && chmod 600 "$GH_CONFIG_DIR/hosts.yml" && chmod 700 "$GH_CONFIG_DIR" || { echo "[pod] gh-login-failed: $(head -c 300 "$HOME/.gh-login.log" 2>/dev/null | tr '\\n' ' ' | sed -E 's/(gh[a-z]_|github_pat_)[A-Za-z0-9_.-]+/\\1***/g')"; exit 7; }` : 'export GH_TOKEN="$(cat /creds/gh-token)"',
|
|
613
620
|
// 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
|
|
614
621
|
...(o.skillEnvs?.length ? [`for n in ${o.skillEnvs.join(' ')}; do [ -d ~/.claude/skills/$n ] && install -m 600 /creds/skillenv-$n ~/.claude/skills/$n/.env; done; echo "[pod] skill env: ${o.skillEnvs.join(',')}"`] : []),
|
|
615
622
|
'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
|
|
@@ -622,9 +629,11 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
622
629
|
`(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
|
|
623
630
|
...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
|
|
624
631
|
// 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
|
|
625
|
-
|
|
632
|
+
o.authorSentence
|
|
633
|
+
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
|
|
634
|
+
: goalPath
|
|
626
635
|
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
|
|
627
|
-
: `elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
|
|
636
|
+
: `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
|
|
628
637
|
'cat /tmp/si.out',
|
|
629
638
|
'[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
|
|
630
639
|
// A long run's log export can pass the 5MB artifact limit; skipping it whole left no window into why a
|
|
@@ -1224,7 +1233,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1224
1233
|
debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
|
|
1225
1234
|
tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
|
|
1226
1235
|
memoryLimit, memoryRequest });
|
|
1227
|
-
const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
|
|
1236
|
+
const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
|
|
1228
1237
|
const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
|
|
1229
1238
|
if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
|
|
1230
1239
|
releaseAdmission?.applied?.(name, context, namespace);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* Cf. PLAN-parallel-self-dev-orchestrator-2026-07-21.
|
|
20
20
|
*/
|
|
21
21
|
import { lstatSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { oldDoorInternalEnv } from '../../self-dev/old-door.js';
|
|
22
23
|
import { selfDevRunsDir } from '../../self-dev/run-store.js';
|
|
23
24
|
import { debug } from '../../debug/log.js';
|
|
24
25
|
import { join, resolve } from 'node:path';
|
|
@@ -356,6 +357,7 @@ export function defaultSelfImplementSpawn(): SelfImplementJobSpawn {
|
|
|
356
357
|
...childNestEnv(),
|
|
357
358
|
...harnessSpaceEnv('self-implement', input.spaceId),
|
|
358
359
|
...executorRoleEnv(), // ⭐ 세포 role 분화 — 자식은 executor(실행자)로 자기인지.
|
|
360
|
+
...oldDoorInternalEnv('self-implement'),
|
|
359
361
|
},
|
|
360
362
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
361
363
|
});
|
package/src/telegram-commands.ts
CHANGED
|
@@ -233,6 +233,11 @@ export function parseTelegramSlash(text: string, commands: TgSlashCommand[]): Tg
|
|
|
233
233
|
* caller (handleIncoming) can send it directly without going through
|
|
234
234
|
* dispatchTelegramSlash when it already has the parse result. */
|
|
235
235
|
export function buildUnknownSlashReply(name: string, commands: TgSlashCommand[]): string {
|
|
236
|
+
const unsupported = buildBotSlashCatalog({
|
|
237
|
+
surface: 'telegram', coreCommands: SLASH_COMMANDS, maturity: FEATURE_MATURITY,
|
|
238
|
+
handledCommands: commands,
|
|
239
|
+
}).unsupportedReply(name);
|
|
240
|
+
if (unsupported) return unsupported;
|
|
236
241
|
const names = commands.map(c => `/${c.name}`).join(' ');
|
|
237
242
|
return `Unknown command: /${name}\nAvailable: ${names}`;
|
|
238
243
|
}
|