elanous 0.2.16 → 0.2.17
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-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/src/agent-mission/driver.ts +53 -10
- package/src/autopilot/track-agent.ts +5 -3
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/harness/harness-cli-command.ts +142 -10
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/index.ts +57 -8
- package/src/intake-plane/digest.ts +39 -4
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/seat-loop/seat-loop.ts +95 -31
- package/src/self-dev/ask-launch-flow.ts +26 -7
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +88 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +19 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/user-config.ts +25 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -43,7 +43,7 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
|
43
43
|
// Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
|
|
44
44
|
export const POD_CHILD_REQUESTS = { cpu: '1', memory: '6Gi' } as const;
|
|
45
45
|
import type { PodPoolMember, PodPoolScheduler } from './pod-pool.js';
|
|
46
|
-
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER } from './pod-lease.js';
|
|
46
|
+
import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
|
|
47
47
|
import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
|
|
48
48
|
import { spawnSync } from 'node:child_process';
|
|
49
49
|
import { createHash } from 'node:crypto';
|
|
@@ -65,7 +65,7 @@ import { goalTypeOf, type GoalType } from '../../../scripts/measure-pod-memory-b
|
|
|
65
65
|
import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-implement/goal-author.js';
|
|
66
66
|
import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
|
|
67
67
|
import { getUserConfig } from '../../user-config.js';
|
|
68
|
-
import { extractPodFailureReason } from './pod-failure-reason.js';
|
|
68
|
+
import { extractPodFailureReason, podTerminalRow } from './pod-failure-reason.js';
|
|
69
69
|
import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
|
|
70
70
|
|
|
71
71
|
export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
|
|
@@ -119,6 +119,8 @@ export interface PodSpawnOptions {
|
|
|
119
119
|
/** Job 수명 상한(초). */
|
|
120
120
|
deadlineSeconds?: number;
|
|
121
121
|
pollMs?: number;
|
|
122
|
+
/** LAUNCH-STALL: ms a launch may wait in one stage before `launch-stalled` (test seam; default harness.launchStallMinutes · 15 min). */
|
|
123
|
+
launchStallMs?: number;
|
|
122
124
|
kubectl?: Kubectl;
|
|
123
125
|
/** Host-side regate (injected for Pod tests). */
|
|
124
126
|
hostRegate?: (input: { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string }) => Promise<HostRegateResult>;
|
|
@@ -143,6 +145,10 @@ export interface PodSpawnOptions {
|
|
|
143
145
|
now?: () => number;
|
|
144
146
|
sleep?: (ms: number) => Promise<void>;
|
|
145
147
|
credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
148
|
+
/** POD-TOKEN-PREREFRESH 시험 심 — «3시간 안 만료» 계정의 호스트 선갱신(기본 refreshCodexAccountHome). */
|
|
149
|
+
codexPrerefresh?: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
150
|
+
/** 시험 심 — 이 호스트가 본부 임대 보유자인가(기본 HQ 펜스 · kubectl 주입 시험이면 false). */
|
|
151
|
+
hqLeaseHolder?: () => boolean | Promise<boolean>;
|
|
146
152
|
grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
|
|
147
153
|
/** 호스트 키 캐시(`~/.cache/<소문자 이름>`)에서 키를 읽는다(시험 주입) — env 에 없을 때. */
|
|
148
154
|
readKeyCache?: (name: string) => string | undefined;
|
|
@@ -173,6 +179,58 @@ export function podJobName(spaceId: string): string {
|
|
|
173
179
|
return `si-${slug || 'job'}-${hash}`.replace(/-+/g, '-').slice(0, 58);
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
/** «3시간 안 만료» — 선갱신 대상이라는 표지(다른 자격 오류와 구분한다). */
|
|
183
|
+
export class PodCodexExpiringError extends Error {
|
|
184
|
+
constructor(readonly account: string, readonly hoursLeft: number) {
|
|
185
|
+
super(`openai-codex:${account} access token 이 3시간 안에 만료 — 호스트에서 먼저 갱신(컨테이너는 갱신 못 한다)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type PodCredentialPrerefreshOutcome = 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease';
|
|
190
|
+
|
|
191
|
+
/** POD-TOKEN-PREREFRESH (10-06) — «3시간 안 만료» 계정은 거부만 하지 말고 선갱신을 한 번 시도한다.
|
|
192
|
+
* 🩸 05:3x team · 09:47 third: 거부만 하고 다음 계정으로 안 넘어가 그 계정으로 뽑힌 Pod 런이 전부 pod-error.
|
|
193
|
+
* ⛔ 갱신은 본부(HQ) 임대 보유자 한 곳에서만 — refresh 토큰은 갱신마다 회전해 두 곳이 갱신하면 로그아웃된다.
|
|
194
|
+
* null = 이 계정은 후보에서 뺀다(호출자가 다음 계정으로). 만료 아닌 다른 자격 오류는 그대로 던진다. */
|
|
195
|
+
export async function podCodexCredentialWithPrerefresh(account: string, deps: {
|
|
196
|
+
credentials: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
|
|
197
|
+
refresh: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
|
|
198
|
+
isLeaseHolder: () => boolean | Promise<boolean>;
|
|
199
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
200
|
+
}): Promise<{ elanousAuth: string; codexAuth: string; ghToken: string } | null> {
|
|
201
|
+
const log = deps.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
202
|
+
try { return deps.credentials(account); }
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!(error instanceof PodCodexExpiringError)) throw error;
|
|
205
|
+
const beforeH = error.hoursLeft;
|
|
206
|
+
const observe = (outcome: PodCredentialPrerefreshOutcome, afterH: number | null, reason?: string) => {
|
|
207
|
+
try { log('pod.credential-prerefresh', outcome, { account, beforeH, afterH, outcome, ...(reason ? { reason } : {}) }); } catch { /* observation is fail-soft */ }
|
|
208
|
+
};
|
|
209
|
+
let holder = false;
|
|
210
|
+
try { holder = await deps.isLeaseHolder(); } catch { holder = false; }
|
|
211
|
+
if (!holder) { observe('no-lease', null); return null; }
|
|
212
|
+
let refreshed: Awaited<ReturnType<typeof deps.refresh>>;
|
|
213
|
+
try { refreshed = await deps.refresh(account); }
|
|
214
|
+
catch { observe('refresh-failed', null, 'threw'); return null; }
|
|
215
|
+
if (!refreshed.ok) { observe('refresh-failed', null, refreshed.kind); return null; }
|
|
216
|
+
try {
|
|
217
|
+
const credential = deps.credentials(account);
|
|
218
|
+
observe('refreshed', refreshed.afterH);
|
|
219
|
+
return credential;
|
|
220
|
+
} catch (again) {
|
|
221
|
+
if (!(again instanceof PodCodexExpiringError)) throw again;
|
|
222
|
+
observe('still-expiring', again.hoursLeft);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 기본 임대 판정 — 기존 HQ 펜스(`ledger-cli` 역할 · fail-open 없음)를 그대로 쓴다. */
|
|
229
|
+
async function defaultHqLeaseHolder(): Promise<boolean> {
|
|
230
|
+
const { hqFenceDecision } = await import('../../hq/hq.js');
|
|
231
|
+
return hqFenceDecision('ledger-cli').run;
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
/** 호스트 계정 → refresh 없는 사본(elanous 저장소 ⊕ codex auth.json) ⊕ gh 토큰. */
|
|
177
235
|
export function hostCredentials(account: string, storePath: string = authStorePath(), ghToken: () => string = defaultGhToken): { elanousAuth: string; codexAuth: string; ghToken: string } {
|
|
178
236
|
// 경로는 해석기로(격리 게이트 · 2026-09-25) — 손으로 `~/.elanous/auth.json` 을 조립하면 test↔prod 격리가 새는 자리가 된다.
|
|
@@ -185,7 +243,7 @@ export function hostCredentials(account: string, storePath: string = authStorePa
|
|
|
185
243
|
const codex = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8')) as { tokens: Record<string, unknown> };
|
|
186
244
|
const access = String(codex.tokens.access_token ?? '');
|
|
187
245
|
const exp = Number(JSON.parse(Buffer.from(access.split('.')[1] ?? '', 'base64url').toString('utf8') || '{}').exp ?? 0);
|
|
188
|
-
if (exp * 1000 - Date.now() < 3 * 3600_000) throw new
|
|
246
|
+
if (exp * 1000 - Date.now() < 3 * 3600_000) throw new PodCodexExpiringError(account, Math.round(((exp * 1000 - Date.now()) / 3_600_000) * 10) / 10);
|
|
189
247
|
const elanousAuth = JSON.stringify({
|
|
190
248
|
version: store.version ?? 1,
|
|
191
249
|
// ⭐ P4(2026-09-26): elanous 사본도 «방금 검사한» codex 홈의 토큰으로 싣는다 — 두 저장소는 따로 갱신된다.
|
|
@@ -600,7 +658,7 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
|
|
|
600
658
|
return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
|
|
601
659
|
}
|
|
602
660
|
|
|
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> {
|
|
661
|
+
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; authorGrade?: 'full' | 'lite'; 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> {
|
|
604
662
|
const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
|
|
605
663
|
const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
|
|
606
664
|
const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
|
|
@@ -629,6 +687,8 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
629
687
|
`(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=$!`,
|
|
630
688
|
...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
|
|
631
689
|
// 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
|
|
690
|
+
// ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
|
|
691
|
+
// lite 는 지금 로컬 저작 전용이다. Pod 저작은 설정·기본(full)을 따른다.
|
|
632
692
|
o.authorSentence
|
|
633
693
|
? `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
694
|
: goalPath
|
|
@@ -865,31 +925,72 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
865
925
|
let releaseAdmission: ((() => void) & { applied?: (job: string, context: string, namespace: string) => void; observed?: () => void }) | undefined;
|
|
866
926
|
try {
|
|
867
927
|
// Every attempt, including an OOM retry, must acquire a pool slot before applying a Job.
|
|
928
|
+
const stallMs = options.launchStallMs ?? (getUserConfig().harness?.launchStallMinutes ?? 15) * 60_000;
|
|
929
|
+
// LAUNCH-STALL (10-06): a stage that waits past the cap says so once — today Pods sat 22 min to 5 h with no event.
|
|
930
|
+
const stalledStages = new Set<string>();
|
|
931
|
+
// pool-slot reason = what THIS launch saw on its last empty tryAcquire; admission reason = the pool-wide lease measurement (one pool, one reading).
|
|
932
|
+
const stageReason: { 'pool-slot'?: string | null } = {};
|
|
933
|
+
const unfitContexts = new Set<string>();
|
|
934
|
+
const watchStall = (stage: 'admission' | 'pool-slot'): (() => void) => {
|
|
935
|
+
const started = Date.now();
|
|
936
|
+
const timer = setTimeout(() => {
|
|
937
|
+
if (stalledStages.has(stage)) return;
|
|
938
|
+
stalledStages.add(stage);
|
|
939
|
+
const pool = options.pool as { waitReason?: () => string | null } | undefined;
|
|
940
|
+
debug.log('harness.launch', 'launch-stalled', { spaceId: input.spaceId, runId: childRunId, stage, waitedSec: Math.round((Date.now() - started) / 1000),
|
|
941
|
+
reason: (stage === 'pool-slot' ? stageReason['pool-slot'] : (typeof pool?.waitReason === 'function' ? pool.waitReason() : null)) ?? 'not-measured-yet' }, { level: 'warn' });
|
|
942
|
+
}, stallMs);
|
|
943
|
+
(timer as { unref?: () => void }).unref?.();
|
|
944
|
+
return () => clearTimeout(timer);
|
|
945
|
+
};
|
|
946
|
+
// Members that can never hold this Job's memory limit are skipped with a reason; all unfit → fail fast instead of waiting forever.
|
|
947
|
+
const unfitFailure = async (): Promise<SelfImplementJobDone | null> => {
|
|
948
|
+
const pool = options.pool as { unfitMembers?: (b: number) => Promise<Array<{ context: string; allocatableMemoryBytes: number }>>; members?: readonly PodPoolMember[] } | undefined;
|
|
949
|
+
if (!pool || typeof pool.unfitMembers !== 'function' || !Array.isArray(pool.members)) return null;
|
|
950
|
+
const limitBytes = memoryQuantityBytes(memoryLimit);
|
|
951
|
+
if (limitBytes === null) return null;
|
|
952
|
+
const unfit = await pool.unfitMembers(limitBytes);
|
|
953
|
+
for (const u of unfit) {
|
|
954
|
+
debug.log('harness.launch', 'launch-member-skipped', { spaceId: input.spaceId, runId: childRunId, member: u.context, reason: 'memory-never-fits', allocatableMemoryBytes: u.allocatableMemoryBytes, memoryLimit }, { level: 'warn' });
|
|
955
|
+
}
|
|
956
|
+
unfitContexts.clear();
|
|
957
|
+
for (const u of unfit) unfitContexts.add(u.context);
|
|
958
|
+
if (!unfit.length || !pool.members.every((m) => unfitContexts.has(m.context))) return null;
|
|
959
|
+
const detail = unfit.map((u) => `${u.context} ${Math.round(u.allocatableMemoryBytes / 2 ** 30 * 10) / 10}GiB`).join(' · ');
|
|
960
|
+
return { exitCode: 1, output: '', error: { code: 'pod-pool-unfit', message: `모든 풀 멤버의 할당 가능 메모리가 이 Job 의 한도 ${memoryLimit} 보다 작다 — 영원히 배치될 수 없다(${detail}) · --pod-memory 를 낮추거나 다른 풀로` } };
|
|
961
|
+
};
|
|
868
962
|
const acquireSlot = async (): Promise<PodPoolMember | null> => {
|
|
869
963
|
if (!options.pool) return null;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
964
|
+
const stop = watchStall('pool-slot');
|
|
965
|
+
try {
|
|
966
|
+
for (;;) {
|
|
967
|
+
if (input.signal?.aborted) return null;
|
|
968
|
+
// The reason comes back inside this call, so a concurrent launch cannot overwrite it.
|
|
969
|
+
const acquired = await options.pool.tryAcquire(unfitContexts, (reason) => { stageReason['pool-slot'] = reason; });
|
|
970
|
+
if (acquired) {
|
|
971
|
+
debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
|
|
972
|
+
return acquired;
|
|
973
|
+
}
|
|
974
|
+
await sleep(options.pollMs ?? 15_000);
|
|
876
975
|
}
|
|
877
|
-
|
|
878
|
-
}
|
|
976
|
+
} finally { stop(); }
|
|
879
977
|
};
|
|
880
978
|
const acquireAdmission = async (): Promise<boolean> => {
|
|
881
979
|
if (!options.pool || typeof options.pool.acquireAdmission !== 'function') return true;
|
|
980
|
+
const stop = watchStall('admission');
|
|
882
981
|
try { releaseAdmission = await options.pool.acquireAdmission(input.signal, after); return true; }
|
|
883
982
|
catch (error) {
|
|
884
983
|
// A predecessor closed without merging is not a cancellation: surface it so the queue owner is notified.
|
|
885
984
|
if (error instanceof Error && error.message.startsWith('pod lease predecessor blocked')) admissionBlocked = error.message;
|
|
886
985
|
return false;
|
|
887
|
-
}
|
|
986
|
+
} finally { stop(); }
|
|
888
987
|
};
|
|
889
988
|
let admissionBlocked: string | undefined;
|
|
890
989
|
const admissionFailure = (): SelfImplementJobDone => admissionBlocked
|
|
891
990
|
? { exitCode: 1, output: '', error: { code: 'pod-predecessor-blocked', message: admissionBlocked } }
|
|
892
991
|
: { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
992
|
+
const unfitDone = await unfitFailure();
|
|
993
|
+
if (unfitDone) return unfitDone;
|
|
893
994
|
if (!await acquireAdmission()) return admissionFailure();
|
|
894
995
|
let member: PodPoolMember | null = await acquireSlot();
|
|
895
996
|
if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
@@ -941,25 +1042,40 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
941
1042
|
for (;;) {
|
|
942
1043
|
try {
|
|
943
1044
|
const grok = options.provider === 'grok';
|
|
944
|
-
const
|
|
945
|
-
debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account, brokered: Boolean(options.accountBroker) });
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
if (
|
|
1045
|
+
const plannedAccount = retryAccount ?? (grok ? 'grok' : options.accountBroker?.() ?? options.account ?? 'team');
|
|
1046
|
+
debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account: plannedAccount, brokered: Boolean(options.accountBroker) });
|
|
1047
|
+
const plannedCodexAccounts = grok || !options.accountBroker || !options.rotationAccounts ? undefined : [plannedAccount, ...options.rotationAccounts.filter((candidate) => candidate !== plannedAccount && candidate !== failedAccount)];
|
|
1048
|
+
if (plannedCodexAccounts && !options.rotationAccounts!.includes(plannedAccount)) throw new Error(`pod: 배분 계정 ${plannedAccount} 이 회전 계획에 없다`);
|
|
1049
|
+
if (plannedCodexAccounts && new Set(options.rotationAccounts).size !== options.rotationAccounts!.length) throw new Error('pod: 유효하고 서로 다른 codex 계정이 필요하다');
|
|
949
1050
|
const accountCredentials = options.credentials ?? hostCredentials;
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
1051
|
+
if (plannedCodexAccounts) podCodexAccountScript(plannedCodexAccounts);
|
|
1052
|
+
// POD-TOKEN-PREREFRESH: «3시간 안 만료» 후보는 선갱신 → 실패면 후보에서 빼고 다음 계정으로.
|
|
1053
|
+
const prerefreshDeps = {
|
|
1054
|
+
credentials: accountCredentials,
|
|
1055
|
+
refresh: options.codexPrerefresh ?? (async (name: string) => (await import('../../oauth/codex.js')).refreshCodexAccountHome(name)),
|
|
1056
|
+
isLeaseHolder: options.hqLeaseHolder ?? (options.kubectl ? () => false : defaultHqLeaseHolder), // kubectl 주입(=시험)이면 본부 임대를 묻지 않는다
|
|
1057
|
+
};
|
|
1058
|
+
const candidates = grok ? [] : plannedCodexAccounts ?? [plannedAccount];
|
|
1059
|
+
const usable: Array<{ name: string; credential: ReturnType<typeof accountCredentials> }> = [];
|
|
1060
|
+
for (const candidate of candidates) {
|
|
1061
|
+
const credential = await podCodexCredentialWithPrerefresh(candidate, prerefreshDeps);
|
|
1062
|
+
if (credential) usable.push({ name: candidate, credential });
|
|
1063
|
+
}
|
|
1064
|
+
if (!grok && usable.length === 0) throw new Error(`openai-codex 후보(${candidates.join(', ')}) access token 이 전부 3시간 안에 만료 — 선갱신도 못 했다(본부 임대 없음 또는 갱신 실패 · pod.credential-prerefresh 를 보라)`);
|
|
1065
|
+
const account = grok ? plannedAccount : usable[0]!.name;
|
|
1066
|
+
if (account !== plannedAccount) debug.log('self-implement.pod', 'account-skipped-expiring', { spaceId: input.spaceId, from: plannedAccount, to: account });
|
|
1067
|
+
const codexAccounts = plannedCodexAccounts ? usable.map((entry) => entry.name) : undefined;
|
|
1068
|
+
const codexCredentials = codexAccounts ? usable.map(({ name: candidate, credential }) => {
|
|
953
1069
|
const elanous = JSON.parse(credential.elanousAuth) as { providers?: Record<string, { tokens?: { refreshToken?: unknown } }> };
|
|
954
1070
|
const codex = JSON.parse(credential.codexAuth) as { tokens?: { refresh_token?: unknown } };
|
|
955
1071
|
if (!elanous.providers?.['openai-codex']?.tokens || elanous.providers['openai-codex'].tokens.refreshToken || !codex.tokens || codex.tokens.refresh_token) {
|
|
956
1072
|
throw new Error(`pod: ${candidate} 자격에 refresh 토큰이 있거나 인증 파일이 없다`);
|
|
957
1073
|
}
|
|
958
1074
|
return credential;
|
|
959
|
-
});
|
|
1075
|
+
}) : undefined;
|
|
960
1076
|
const creds = grok
|
|
961
1077
|
? (options.grokCredentials ?? (() => hostGrokCredentials({ env, apiKeyOptIn: options.grokApiKeyOptIn })))()
|
|
962
|
-
: codexCredentials?.[0] ??
|
|
1078
|
+
: codexCredentials?.[0] ?? usable[0]!.credential;
|
|
963
1079
|
if (grok && !('grokAuth' in creds && creds.grokAuth) && !('grokApiKey' in creds && creds.grokApiKey && options.grokApiKeyOptIn === true)) {
|
|
964
1080
|
throw new Error('grok: 구독 자격 없음 · API 키 opt-in 꺼짐 또는 키 없음');
|
|
965
1081
|
}
|
|
@@ -1211,12 +1327,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1211
1327
|
? String(detail.until) : undefined;
|
|
1212
1328
|
const armEnv = Object.fromEntries(Object.entries(options.armEnv ?? {}).filter(([key]) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL' && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV && (!hostRefresh || !['GH_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR'].includes(key))));
|
|
1213
1329
|
const seat = env.ELANOUS_HARNESS_SEAT;
|
|
1214
|
-
const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat
|
|
1330
|
+
const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat || env.ELANOUS_POD_AUTHOR_ON_POD === '1'
|
|
1215
1331
|
? {
|
|
1216
1332
|
...armEnv,
|
|
1217
1333
|
...(detailUntil ? { ELANOUS_LIVE_DETAIL_UNTIL: detailUntil } : {}),
|
|
1218
1334
|
...(env.ELANOUS_DISPATCH_RECORDED === '1' ? { ELANOUS_DISPATCH_RECORDED: '1' } : {}),
|
|
1219
1335
|
...(seat === 'OP' || seat === 'TC' || seat === 'MK' || seat === 'UX' ? { ELANOUS_HARNESS_SEAT: seat } : {}),
|
|
1336
|
+
...(env.ELANOUS_POD_AUTHOR_ON_POD === '1'
|
|
1337
|
+
? { ELANOUS_POD_AUTHOR_ON_POD: '1', ...(env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'flag' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'config' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'default'
|
|
1338
|
+
? { ELANOUS_POD_AUTHOR_GRADE_SOURCE: env.ELANOUS_POD_AUTHOR_GRADE_SOURCE, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { ELANOUS_POD_AUTHOR_GRADE: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}) } : {}),
|
|
1220
1339
|
...(grounding ? { [GROUNDING_URL_ENV]: groundingUrl! } : {}),
|
|
1221
1340
|
...(credentialRelay ? { [POD_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GROK_PATH}` } : {}),
|
|
1222
1341
|
...(githubRelay ? { [POD_GITHUB_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GITHUB_PATH}` } : {}),
|
|
@@ -1233,7 +1352,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1233
1352
|
debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
|
|
1234
1353
|
tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
|
|
1235
1354
|
memoryLimit, memoryRequest });
|
|
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 를 부르지 않는다
|
|
1355
|
+
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, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), 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 를 부르지 않는다
|
|
1237
1356
|
const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
|
|
1238
1357
|
if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
|
|
1239
1358
|
releaseAdmission?.applied?.(name, context, namespace);
|
|
@@ -1477,6 +1596,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
|
|
|
1477
1596
|
if (member) { options.pool!.release(member); member = null; }
|
|
1478
1597
|
releaseAdmission?.();
|
|
1479
1598
|
releaseAdmission = undefined;
|
|
1599
|
+
const unfitRetry = await unfitFailure();
|
|
1600
|
+
if (unfitRetry) return unfitRetry;
|
|
1480
1601
|
if (!await acquireAdmission()) return admissionFailure();
|
|
1481
1602
|
member = await acquireSlot();
|
|
1482
1603
|
if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
|
|
@@ -1602,12 +1723,13 @@ function lastPodChildFailure(logs: string): { stage: string; error: string } | n
|
|
|
1602
1723
|
try { result = JSON.parse(line); } catch { continue; }
|
|
1603
1724
|
if (!result || typeof result !== 'object' || Array.isArray(result)) continue;
|
|
1604
1725
|
const row = result as Record<string, unknown>;
|
|
1605
|
-
|
|
1606
|
-
if (
|
|
1607
|
-
|
|
1726
|
+
const terminal = podTerminalRow(row);
|
|
1727
|
+
if (!terminal) continue;
|
|
1728
|
+
if (terminal.ok !== false || typeof terminal.error !== 'string') return null;
|
|
1729
|
+
const lines = terminal.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
|
|
1608
1730
|
const first = lines[0]!.slice(0, 240);
|
|
1609
1731
|
const last = lines.at(-1)!.slice(0, 240);
|
|
1610
|
-
return { stage:
|
|
1732
|
+
return { stage: terminal.stage, error: first === last ? first : `${first}\n${last}` };
|
|
1611
1733
|
}
|
|
1612
1734
|
return null;
|
|
1613
1735
|
}
|
package/src/telegram-commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dashboardLoops } from './domains/dashboard-data.js';
|
|
2
2
|
import { readSchedulesInventory } from './nexus/api/schedules-read.js';
|
|
3
|
-
import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH
|
|
3
|
+
import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH } from './loops/status-rows.js';
|
|
4
4
|
|
|
5
5
|
interface LoopsSources {
|
|
6
6
|
schedules: () => unknown;
|
|
@@ -25,20 +25,27 @@ export async function telegramLoopsStatus(args: string[], sources: LoopsSources
|
|
|
25
25
|
if (path === LOOPS_PATH) return sources.loops();
|
|
26
26
|
throw new Error('unknown inventory');
|
|
27
27
|
}, sources.now());
|
|
28
|
-
if (!rows.length) return '
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if (!rows.length) return '모두 정상';
|
|
29
|
+
const problems = rows.filter(row => row.verdict === '늦음' || row.verdict === '실패');
|
|
30
|
+
const ordered = [
|
|
31
|
+
...problems.filter(row => row.verdict === '늦음'),
|
|
32
|
+
...problems.filter(row => row.verdict === '실패'),
|
|
33
|
+
];
|
|
34
|
+
const summary = (['살아 있음', '꺼짐', '판정 불가'] as const)
|
|
35
|
+
.map(verdict => `${verdict} ${rows.filter(row => row.verdict === verdict).length}`).join(' · ');
|
|
36
|
+
const lines = ['루프·크론 현황'];
|
|
37
|
+
let shown = 0;
|
|
38
|
+
for (const row of ordered.slice(0, 10)) {
|
|
39
|
+
const line = `• ${row.verdict === '늦음' ? '🔴' : '❌'} ${row.verdict} · ${clean(row.name)} (${clean(row.layer)}) · ${clean(row.mode)} · 마지막 ${row.lastRun ?? '기록 없음'} · ${clean(row.id)}`;
|
|
40
|
+
// Leave room for the summary and an omission count under Telegram's 4096-character cap.
|
|
41
|
+
if (lines.join('\n').length + line.length + summary.length + 60 > 4000) break;
|
|
42
|
+
lines.push(line);
|
|
43
|
+
shown++;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
if (shown < ordered.length) lines.push(`… ${ordered.length - shown}개 더`);
|
|
46
|
+
lines.push(summary);
|
|
47
|
+
return lines.join('\n');
|
|
41
48
|
} catch {
|
|
42
|
-
return '루프
|
|
49
|
+
return '루프 현황 못 읽음';
|
|
43
50
|
}
|
|
44
51
|
}
|
package/src/user-config.ts
CHANGED
|
@@ -3618,9 +3618,13 @@ export interface UserConfig {
|
|
|
3618
3618
|
loops?: { owners?: Record<string, EventSeat>; defaultOwner?: EventSeat; orchestrator?: OrchestratorLoopConfig; steward?: { mode?: StewardLoopMode; linearTeam?: string; roles?: Record<string, { maxConcurrent?: number }>; budget?: number; tracks?: Record<string, string>; alertAfterFailures?: number; launch?: 'off' | 'shadow' | 'live'; maxParallel?: number; podPool?: string }; seat?: SeatLoopConfig; persona?: PersonaLoopConfig };
|
|
3619
3619
|
/** 🔐 Pod 의 Grok API 키 과금은 명시 opt-in 만. 생략·잘못된 값은 false.
|
|
3620
3620
|
* budgetGate 와 같은 `harness` 객체다 — 중복 선언이면 뒤 타입이 앞을 지운다. */
|
|
3621
|
-
harness?: { revertGuard?: { depth: number }; pod?: { grokApiKeyOptIn: boolean }; budgetGate?: HarnessBudgetGateConfig; exposeGate?: 'warn' | 'strict'; difficultyPlacement?: boolean; defaultRepo?: string; /** Optional GitHub repository name (owner/repo), distinct from the local defaultRepo path. */ repo?: string; substrate?: 'local' | 'pod'; podPool?: string; worktreeAddTimeoutSec?: number; queue?: { seatCap?: Partial<Record<'OP' | 'TC' | 'MK' | 'UX', number
|
|
3621
|
+
harness?: { edgeRail?: { canaryOkRuns?: number; failureMultiplier?: number; minSamples?: number }; /** Goal authoring grade; absent or invalid config resolves to full. */ authorGrade?: 'full' | 'lite'; revertGuard?: { depth: number }; pod?: { grokApiKeyOptIn: boolean }; budgetGate?: HarnessBudgetGateConfig; exposeGate?: 'warn' | 'strict'; difficultyPlacement?: boolean; defaultRepo?: string; /** Optional GitHub repository name (owner/repo), distinct from the local defaultRepo path. */ repo?: string; substrate?: 'local' | 'pod'; podPool?: string; worktreeAddTimeoutSec?: number; queue?: { seatCap?: Partial<Record<'OP' | 'TC' | 'MK' | 'UX', number>>; /** Direct `harness say|ask` goes through the seat queue first (ONEDOOR-2). Absent = env ELANOUS_HARNESS_QUEUE_DIRECT_SAY, else off. */ directSay?: boolean }; /** `harness say` 문장을 Pod 안에서 저작부터 돌릴지. 생략·false = 끔(호스트가 저작). */ authorOnPod?: boolean; /** Stopped-PR repair goals: shadow records only (default); live enqueues them. */ helper?: { repair?: 'shadow' | 'live'; /** Live launches allowed per UTC day. Absent or invalid means 3. */ repairPerDay?: number };
|
|
3622
3622
|
/** Nested elanous launches. Only depth 0 may set `allow`. Absent or any other value refuses. A depth >= 1 `--nested-elanous allow` is ignored. */
|
|
3623
3623
|
nestedElanous?: 'allow' | 'refuse';
|
|
3624
|
+
/** Maximum nested elanous depth; safe positive integer, default 2. */
|
|
3625
|
+
nestedElanousMaxDepth?: number;
|
|
3626
|
+
/** Minutes a Pod launch may wait in one stage (admission · pool slot) before a `launch-stalled` event; positive number, default 15. */
|
|
3627
|
+
launchStallMinutes?: number;
|
|
3624
3628
|
/** Follow-up goal after a merged harness PR. Omitted and anything but `live` record only. */
|
|
3625
3629
|
followUp?: 'shadow' | 'live';
|
|
3626
3630
|
};
|
|
@@ -4022,7 +4026,7 @@ function defaultConfig(): UserConfig {
|
|
|
4022
4026
|
nativeStructure: { ...TOOLS_DEFAULTS.nativeStructure },
|
|
4023
4027
|
selfImplement: { ...TOOLS_DEFAULTS.selfImplement },
|
|
4024
4028
|
},
|
|
4025
|
-
harness: { revertGuard: { depth: 50 }, budgetGate: { ...DEFAULT_BUDGET_GATE }, exposeGate: 'warn', difficultyPlacement: false, followUp: 'shadow' },
|
|
4029
|
+
harness: { revertGuard: { depth: 50 }, nestedElanousMaxDepth: 2, budgetGate: { ...DEFAULT_BUDGET_GATE }, exposeGate: 'warn', difficultyPlacement: false, followUp: 'shadow' },
|
|
4026
4030
|
grounding: { sources: [] },
|
|
4027
4031
|
raw: {},
|
|
4028
4032
|
};
|
|
@@ -4741,6 +4745,15 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
|
|
|
4741
4745
|
})() },
|
|
4742
4746
|
},
|
|
4743
4747
|
harness: {
|
|
4748
|
+
edgeRail: (() => {
|
|
4749
|
+
const value = harness.edgeRail && typeof harness.edgeRail === 'object' && !Array.isArray(harness.edgeRail)
|
|
4750
|
+
? harness.edgeRail as Record<string, unknown> : {};
|
|
4751
|
+
return {
|
|
4752
|
+
canaryOkRuns: typeof value.canaryOkRuns === 'number' && Number.isSafeInteger(value.canaryOkRuns) && value.canaryOkRuns > 0 ? value.canaryOkRuns : 3,
|
|
4753
|
+
failureMultiplier: typeof value.failureMultiplier === 'number' && Number.isFinite(value.failureMultiplier) && value.failureMultiplier > 1 ? value.failureMultiplier : 2,
|
|
4754
|
+
minSamples: typeof value.minSamples === 'number' && Number.isSafeInteger(value.minSamples) && value.minSamples > 0 ? value.minSamples : 30,
|
|
4755
|
+
};
|
|
4756
|
+
})(),
|
|
4744
4757
|
revertGuard: { depth: (() => {
|
|
4745
4758
|
const value = harness.revertGuard && typeof harness.revertGuard === 'object' && !Array.isArray(harness.revertGuard)
|
|
4746
4759
|
? (harness.revertGuard as Record<string, unknown>).depth : undefined;
|
|
@@ -4754,7 +4767,10 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
|
|
|
4754
4767
|
&& (harness.queue as Record<string, unknown>).seatCap && typeof (harness.queue as Record<string, unknown>).seatCap === 'object'
|
|
4755
4768
|
&& !Array.isArray((harness.queue as Record<string, unknown>).seatCap)
|
|
4756
4769
|
? (harness.queue as { seatCap: Record<string, unknown> }).seatCap : {},
|
|
4757
|
-
).filter(([seat, cap]) => /^(OP|TC|MK|UX)$/.test(seat) && typeof cap === 'number' && Number.isSafeInteger(cap) && cap > 0))
|
|
4770
|
+
).filter(([seat, cap]) => /^(OP|TC|MK|UX)$/.test(seat) && typeof cap === 'number' && Number.isSafeInteger(cap) && cap > 0)),
|
|
4771
|
+
...(harness.queue && typeof harness.queue === 'object' && !Array.isArray(harness.queue)
|
|
4772
|
+
&& typeof (harness.queue as Record<string, unknown>).directSay === 'boolean'
|
|
4773
|
+
? { directSay: (harness.queue as { directSay: boolean }).directSay } : {}) },
|
|
4758
4774
|
...(harness.substrate === 'local' || harness.substrate === 'pod' ? { substrate: harness.substrate } : {}),
|
|
4759
4775
|
...(typeof harness.podPool === 'string' && harness.podPool.trim() ? { podPool: harness.podPool.trim() } : {}),
|
|
4760
4776
|
...(typeof harness.worktreeAddTimeoutSec === 'number' && Number.isSafeInteger(harness.worktreeAddTimeoutSec)
|
|
@@ -4774,9 +4790,15 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
|
|
|
4774
4790
|
? repairPerDay : 3,
|
|
4775
4791
|
};
|
|
4776
4792
|
})(),
|
|
4793
|
+
...(harness.authorGrade === 'lite' || harness.authorGrade === 'full' ? { authorGrade: harness.authorGrade } : {}),
|
|
4777
4794
|
...(harness.authorOnPod === true ? { authorOnPod: true } : {}),
|
|
4778
4795
|
...(harness.nestedElanous === 'allow' || harness.nestedElanous === 'refuse'
|
|
4779
4796
|
? { nestedElanous: harness.nestedElanous } : {}),
|
|
4797
|
+
nestedElanousMaxDepth: typeof harness.nestedElanousMaxDepth === 'number'
|
|
4798
|
+
&& Number.isSafeInteger(harness.nestedElanousMaxDepth) && harness.nestedElanousMaxDepth > 0
|
|
4799
|
+
? harness.nestedElanousMaxDepth : 2,
|
|
4800
|
+
...(typeof harness.launchStallMinutes === 'number' && Number.isFinite(harness.launchStallMinutes) && harness.launchStallMinutes > 0
|
|
4801
|
+
? { launchStallMinutes: harness.launchStallMinutes } : {}),
|
|
4780
4802
|
followUp: harness.followUp === 'live' ? 'live' : 'shadow',
|
|
4781
4803
|
budgetGate: parseHarnessBudgetGate(
|
|
4782
4804
|
rawObj.harness && typeof rawObj.harness === 'object' && !Array.isArray(rawObj.harness)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"214bcf402de4f243b7f348ab7674b06d9cac5465"}
|