@buzzni/saycode-cli 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/index.mjs +517 -216
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,7 +131,12 @@ happy agent stop <id> # 사용자가 명시한 경우에만 현재 응
|
|
|
131
131
|
`steer` 성공의 `accepted:true`는 세션 RPC가 요청을 받았다는 뜻이며 실제 transcript 전달 완료를
|
|
132
132
|
뜻하지 않습니다(`delivered:false`). 끝난 턴의 후속 작업에는 `prompt`를 사용합니다. 사람이 시작한 세션
|
|
133
133
|
(`depth` 0)은 스코프 안 모든 세션에 `steer`와 `stop`을 보낼 수 있고, spawn된 세션은 원장에 기록된 최대
|
|
134
|
-
2단계 자식만 허용하며, `stop`은 보관·삭제를 하지 않습니다.
|
|
134
|
+
2단계 자식만 허용하며, `stop`은 보관·삭제를 하지 않습니다. spawn 한도(`whoami`의 `maxSpawn`)는 세션이
|
|
135
|
+
도는 머신의 코어·메모리에서 산정하므로 머신마다 다르고, 최근 6시간 안의 spawn만 세는 롤링 윈도우라
|
|
136
|
+
소진돼도 시간이 지나면 회복됩니다(`budgetResetsAt`, ADR-063). 데몬이 확인하지 못한 시도도 같은
|
|
137
|
+
6시간을 차감합니다 — 응답만 유실됐을 뿐 세션은 만들어졌을 수 있기 때문입니다. 깊이 상한에는 이런
|
|
138
|
+
회복이 없고, 에이전트 세션 안에서는 `chat`으로 **새** 대화를 시작할 수 없습니다(기존 세션 attach는
|
|
139
|
+
가능). 예산과 계보를 적용하는 경로가 `agent spawn` 하나뿐이어야 하기 때문입니다. 이 원장 검사는
|
|
135
140
|
같은 OS 사용자 아래의 오조작을 줄이는 advisory 가드레일이며 보안 경계나 서버 측 권한 증명은 아닙니다.
|
|
136
141
|
같은 worktree에는 동시에 둘 이상의 쓰기 자식을 만들지 말고 격리가 필요하면 Desktop의 관리형
|
|
137
142
|
worktree 병렬 실행을 사용하세요.
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import { homedir, tmpdir, platform } from "node:os";
|
|
3
|
+
import { homedir, tmpdir, platform, totalmem, cpus } from "node:os";
|
|
4
4
|
import require$$0$4, { Buffer as Buffer$1 } from "buffer";
|
|
5
5
|
import require$$0 from "crypto";
|
|
6
6
|
import require$$0$1 from "fs";
|
|
@@ -16,7 +16,7 @@ import require$$3$1 from "net";
|
|
|
16
16
|
import require$$4$1 from "tls";
|
|
17
17
|
import require$$0$5 from "stream";
|
|
18
18
|
import require$$0$3 from "zlib";
|
|
19
|
-
import { readFileSync, mkdirSync, writeFileSync, rmSync, chmodSync, constants as constants$1, appendFileSync, accessSync, statSync } from "node:fs";
|
|
19
|
+
import { readFileSync, mkdirSync, writeFileSync, rmSync, chmodSync, constants as constants$1, appendFileSync, openSync, fstatSync, readSync, closeSync, accessSync, statSync } from "node:fs";
|
|
20
20
|
import { dirname, join, basename, extname, delimiter } from "node:path";
|
|
21
21
|
import { readFile, stat, mkdtemp, access, rm, mkdir, appendFile, writeFile, chmod } from "node:fs/promises";
|
|
22
22
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -40,7 +40,7 @@ function _mergeNamespaces(n, m) {
|
|
|
40
40
|
}
|
|
41
41
|
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
|
|
42
42
|
}
|
|
43
|
-
const version$1 = "0.
|
|
43
|
+
const version$1 = "0.5.1";
|
|
44
44
|
const manifest = {
|
|
45
45
|
version: version$1
|
|
46
46
|
};
|
|
@@ -271,6 +271,15 @@ const machineUsageKo = {
|
|
|
271
271
|
"machineUsage.pollingNote": "{minutes}분 전 · 머신당 5분마다 한 번만 조회",
|
|
272
272
|
"machineUsage.autoSwitched": "{provider} 계정을 자동 전환했습니다: {from} → {to}"
|
|
273
273
|
};
|
|
274
|
+
const hubSessionKo = {
|
|
275
|
+
"hubSession.enable": "이 대화를 프로젝트 허브로",
|
|
276
|
+
"hubSession.disable": "프로젝트 허브 해제",
|
|
277
|
+
"hubSession.hint": "같은 프로젝트의 다른 대화가 턴을 끝내거나 입력을 기다리면 이 대화에 메시지로 알려줍니다",
|
|
278
|
+
"hubSession.newSessionLabel": "이 대화를 프로젝트 허브로 시작",
|
|
279
|
+
"hubSession.newSessionHint": "다른 대화의 완료·질문을 여기로 모읍니다",
|
|
280
|
+
"hubSession.newSessionReplaces": '지금 허브는 "{title}" — 켜면 이 대화로 바뀝니다',
|
|
281
|
+
"hubSession.newSessionParallelUnavailable": "병렬로 여러 대화를 시작할 때는 허브를 지정할 수 없습니다 — 시작한 뒤 헤더에서 켜세요"
|
|
282
|
+
};
|
|
274
283
|
const taskNotificationKo = {
|
|
275
284
|
"taskNotification.ariaLabel": "백그라운드 작업 알림",
|
|
276
285
|
"taskNotification.status.completed": "작업 완료",
|
|
@@ -1005,7 +1014,11 @@ const memoryEventsKo = {
|
|
|
1005
1014
|
"settings.memoryEvents.unavailable": "기억 대시보드가 실행 중이 아닙니다.",
|
|
1006
1015
|
"settings.memoryEvents.searchLabel": "기억 검색",
|
|
1007
1016
|
"settings.memoryEvents.prevPage": "이전",
|
|
1008
|
-
"settings.memoryEvents.nextPage": "다음"
|
|
1017
|
+
"settings.memoryEvents.nextPage": "다음",
|
|
1018
|
+
"settings.memoryEvents.delete": "삭제",
|
|
1019
|
+
"settings.memoryEvents.confirmDelete": "정말 삭제",
|
|
1020
|
+
"settings.memoryEvents.deleteHint": "삭제한 기억은 되돌릴 수 없습니다.",
|
|
1021
|
+
"settings.memoryEvents.deleteFailed": "삭제하지 못했습니다: {reason}"
|
|
1009
1022
|
};
|
|
1010
1023
|
const externalWorktreeInboxKo = {
|
|
1011
1024
|
"externalWorktreeInbox.title": "앱 밖에서 만든 worktree",
|
|
@@ -1218,7 +1231,46 @@ const accountSettingsKo = {
|
|
|
1218
1231
|
"settingsGroup.devices": "기기",
|
|
1219
1232
|
"settingsGroup.app": "앱",
|
|
1220
1233
|
"shortcuts.action.settings": "환경설정 열기",
|
|
1221
|
-
"shortcuts.action.settingsDescription": "환경설정 창을 엽니다."
|
|
1234
|
+
"shortcuts.action.settingsDescription": "환경설정 창을 엽니다.",
|
|
1235
|
+
"accountKeyRecovery.title": "암호화된 데이터가 잠겨 있습니다",
|
|
1236
|
+
"accountKeyRecovery.description": "이 데스크톱에서 암호화된 데이터를 잠금 해제하려면 E2EE 복구 키를 입력하세요. 복구 키는 이 기기를 벗어나지 않습니다.",
|
|
1237
|
+
"accountKeyRecovery.label": "E2EE 복구 키",
|
|
1238
|
+
"accountKeyRecovery.unavailable": "이 계정에서는 복구 키 잠금 해제를 사용할 수 없습니다.",
|
|
1239
|
+
"accountKeyRecovery.invalid": "복구 키로 암호화된 데이터를 잠금 해제하지 못했습니다.",
|
|
1240
|
+
"accountKeyRecovery.failed": "복구 키 잠금 해제에 실패했습니다.",
|
|
1241
|
+
"accountKeyRecovery.action": "암호화된 데이터 잠금 해제",
|
|
1242
|
+
"accountKeyRecovery.busy": "잠금 해제 중…",
|
|
1243
|
+
"accountKeyDevice.title": "신뢰 기기",
|
|
1244
|
+
"accountKeyDevice.description": "기존 신뢰 기기에서 이 데스크톱을 승인하거나 대기 중인 기기 요청을 검토하세요.",
|
|
1245
|
+
"accountKeyDevice.loading": "신뢰 기기 상태를 불러오는 중…",
|
|
1246
|
+
"accountKeyDevice.loadFailed": "기기 승인 상태를 불러오지 못했습니다.",
|
|
1247
|
+
"accountKeyDevice.stableIdentityUnavailable": "안정적인 기기 ID를 사용할 수 없어 신뢰 기기 등록을 진행할 수 없습니다.",
|
|
1248
|
+
"accountKeyDevice.secureStorageUnavailable": "보안 기기 저장소를 사용할 수 없어 신뢰 기기 등록을 진행할 수 없습니다.",
|
|
1249
|
+
"accountKeyDevice.qrFailed": "기기 승인 QR을 만들지 못했습니다.",
|
|
1250
|
+
"accountKeyDevice.qrAlt": "기기 승인 QR 코드",
|
|
1251
|
+
"accountKeyDevice.requestFailed": "기기 승인 요청에 실패했습니다.",
|
|
1252
|
+
"accountKeyDevice.pending": "신뢰 기기의 승인을 기다리고 있습니다.",
|
|
1253
|
+
"accountKeyDevice.completeFailed": "기기 승인을 완료하지 못했습니다.",
|
|
1254
|
+
"accountKeyDevice.approved": "이 데스크톱이 신뢰 기기로 승인되었습니다.",
|
|
1255
|
+
"accountKeyDevice.cancelled": "이 기기에서 승인 요청을 취소했습니다.",
|
|
1256
|
+
"accountKeyDevice.cancelFailed": "기기 승인 요청을 취소하지 못했습니다.",
|
|
1257
|
+
"accountKeyDevice.requestsFailed": "대기 중인 기기 요청을 불러오지 못했습니다.",
|
|
1258
|
+
"accountKeyDevice.approveFailed": "기기를 승인하지 못했습니다.",
|
|
1259
|
+
"accountKeyDevice.rejectFailed": "기기 요청을 거절하지 못했습니다.",
|
|
1260
|
+
"accountKeyDevice.revokeFailed": "신뢰 기기를 폐기하지 못했습니다.",
|
|
1261
|
+
"accountKeyDevice.cleanupFailed": "기기는 폐기됐지만 로컬 자격 증명을 삭제하지 못했습니다.",
|
|
1262
|
+
"accountKeyDevice.lockPersistFailed": "기기는 폐기됐지만 로컬 세션 잠금을 저장하지 못했습니다.",
|
|
1263
|
+
"accountKeyDevice.request": "기기 승인 요청",
|
|
1264
|
+
"accountKeyDevice.check": "승인 확인",
|
|
1265
|
+
"accountKeyDevice.cancel": "요청 취소",
|
|
1266
|
+
"accountKeyDevice.scanBefore": "{time} 전에 승인된 기기에서 이 QR을 스캔하세요.",
|
|
1267
|
+
"accountKeyDevice.expires": "{time}에 만료",
|
|
1268
|
+
"accountKeyDevice.approve": "기기 승인",
|
|
1269
|
+
"accountKeyDevice.reject": "기기 거절",
|
|
1270
|
+
"accountKeyDevice.refresh": "요청 새로 고침",
|
|
1271
|
+
"accountKeyDevice.current": "현재 데스크톱",
|
|
1272
|
+
"accountKeyDevice.lastUsed": "마지막 사용: {time}",
|
|
1273
|
+
"accountKeyDevice.revoke": "신뢰 기기 폐기"
|
|
1222
1274
|
};
|
|
1223
1275
|
const projectScopeKo = {
|
|
1224
1276
|
"projectScope.protected": "프로젝트 범위 보호됨",
|
|
@@ -1508,6 +1560,8 @@ const terminalFontKo = {
|
|
|
1508
1560
|
const subagentKo = {
|
|
1509
1561
|
"subagent.title": "자식 에이전트 {count}개",
|
|
1510
1562
|
"subagent.open": "자식 대화 열기",
|
|
1563
|
+
"subagent.parent.link": "부모 에이전트로 이동: {title}",
|
|
1564
|
+
"subagent.parent.untitled": "제목 없는 대화",
|
|
1511
1565
|
"subagent.status.responding": "응답 중",
|
|
1512
1566
|
"subagent.status.waiting": "입력 대기",
|
|
1513
1567
|
"subagent.status.completed": "완료",
|
|
@@ -1556,6 +1610,7 @@ const composerQuickCommandsKo = {
|
|
|
1556
1610
|
const aiCommitPrKo = {
|
|
1557
1611
|
"aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
|
|
1558
1612
|
"aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
|
|
1613
|
+
"aiCommitPr.stagedUnknown": "스테이지된 변경을 확인하지 못했습니다.",
|
|
1559
1614
|
"aiCommitPr.stageAll": "모든 변경 스테이지",
|
|
1560
1615
|
"aiCommitPr.staging": "스테이지하는 중…",
|
|
1561
1616
|
"aiCommitPr.messagePlaceholder": "커밋 메시지",
|
|
@@ -1633,6 +1688,14 @@ const connectorReadinessKo = {
|
|
|
1633
1688
|
"connectorReadiness.state.unknown": "상태 미확인"
|
|
1634
1689
|
};
|
|
1635
1690
|
const checkpointKo = {
|
|
1691
|
+
"checkpoint.activation.label": "파일 체크포인트 보호",
|
|
1692
|
+
"checkpoint.activation.title": "에이전트가 파일을 변경하기 전에 세션별 체크포인트를 만듭니다.",
|
|
1693
|
+
"checkpoint.activation.hint": "세션별",
|
|
1694
|
+
"checkpoint.activation.unavailable.platform": "macOS에서만 지원",
|
|
1695
|
+
"checkpoint.activation.unavailable.agent": "Claude와 Codex만 지원",
|
|
1696
|
+
"checkpoint.activation.unavailable.sandbox": "프로젝트 샌드박스 필요",
|
|
1697
|
+
"checkpoint.activation.unavailable.worktree": "작업 사본은 준비 중",
|
|
1698
|
+
"checkpoint.activation.error": "파일 체크포인트 보호를 사용할 수 없습니다: {reason}",
|
|
1636
1699
|
"checkpoint.title": "파일 체크포인트",
|
|
1637
1700
|
"checkpoint.status.protected": "보호됨",
|
|
1638
1701
|
"checkpoint.status.legacy": "보호되지 않음",
|
|
@@ -1793,7 +1856,7 @@ const settingsInfoStructureKo = {
|
|
|
1793
1856
|
"overflowMenu.aria": "추가 액션 메뉴",
|
|
1794
1857
|
"overflowMenu.title": "추가 액션"
|
|
1795
1858
|
};
|
|
1796
|
-
const catalogExtensionsKo = { ...taskNotificationKo, ...machineUsageKo, ...completionInboxKo, ...agentBoardKo, ...fanoutLifecycleKo, ...conversationSearchKo, ...browserCookieImportKo, ...orcaPortKo, ...usageAnalyticsKo, ...outboundWebhookKo, ...scheduledAutomationsKo, ...githubTriggersKo, ...memoryHubKo, ...extensionPanelKo, ...extensionInstallKo, ...personalChatsKo, ...newSessionStarterKo, ...browserConsoleCaptureKo, ...completionSignalKo, ...agentLoopDetectionKo, ...operationModelRetryKo, ...skillCatalogKo, ...ciAutoFixKo, ...collaborationKo, ...backgroundTaskKo, ...sessionTitleKo, ...accountSettingsKo, ...projectScopeKo, ...machineSettingsConsoleKo, ...sessionTokenUsageKo, ...worktreeSetupKo, ...connectionIndicatorKo, ...voiceDictationKo, ...newSessionProfileKo, ...agentBoardBroadcastKo, ...judgementEvidenceKo, ...releaseNoteKo, ...notificationSoundKo, ...officeViewerKo, ...additionalDirectoriesKo, ...agentHandoffKo, ...sidebarUpdateKo, ...autopilotSelfReviewRoundsKo, ...aiCommitPrKo, ...externalWorktreeInboxKo, ...externalSessionVaultKo, ...orgSearchKo, ...terminalFontKo, ...globalPauseKo, ...subagentKo, ...reviewNavigationKo, ...projectLinksKo, ...projectHandoffKo, ...chatFolderUploadKo, ...composerAddMenuKo, ...composerQuickCommandsKo, ...connectorReadinessKo, ...settingsInfoStructureKo, ...localLoopKo, ...localizationGapKo, ...checkpointKo, ...memoryGuardrailsKo, ...memoryEventsKo };
|
|
1859
|
+
const catalogExtensionsKo = { ...taskNotificationKo, ...machineUsageKo, ...completionInboxKo, ...agentBoardKo, ...fanoutLifecycleKo, ...conversationSearchKo, ...browserCookieImportKo, ...orcaPortKo, ...usageAnalyticsKo, ...outboundWebhookKo, ...scheduledAutomationsKo, ...githubTriggersKo, ...memoryHubKo, ...extensionPanelKo, ...extensionInstallKo, ...personalChatsKo, ...newSessionStarterKo, ...browserConsoleCaptureKo, ...completionSignalKo, ...agentLoopDetectionKo, ...operationModelRetryKo, ...skillCatalogKo, ...ciAutoFixKo, ...collaborationKo, ...backgroundTaskKo, ...sessionTitleKo, ...accountSettingsKo, ...projectScopeKo, ...machineSettingsConsoleKo, ...sessionTokenUsageKo, ...worktreeSetupKo, ...connectionIndicatorKo, ...voiceDictationKo, ...newSessionProfileKo, ...agentBoardBroadcastKo, ...judgementEvidenceKo, ...releaseNoteKo, ...notificationSoundKo, ...officeViewerKo, ...additionalDirectoriesKo, ...agentHandoffKo, ...sidebarUpdateKo, ...autopilotSelfReviewRoundsKo, ...aiCommitPrKo, ...externalWorktreeInboxKo, ...externalSessionVaultKo, ...orgSearchKo, ...terminalFontKo, ...globalPauseKo, ...subagentKo, ...reviewNavigationKo, ...projectLinksKo, ...projectHandoffKo, ...chatFolderUploadKo, ...composerAddMenuKo, ...composerQuickCommandsKo, ...connectorReadinessKo, ...settingsInfoStructureKo, ...localLoopKo, ...localizationGapKo, ...checkpointKo, ...memoryGuardrailsKo, ...memoryEventsKo, ...hubSessionKo };
|
|
1797
1860
|
const onboardingKo = {
|
|
1798
1861
|
"boot.stage.app": "앱 준비 중…",
|
|
1799
1862
|
"boot.stage.localRuntime": "로컬 작업공간 준비 중…",
|
|
@@ -2605,6 +2668,7 @@ const ko = {
|
|
|
2605
2668
|
"gitControls.branchLoading": "Git branch 확인 중",
|
|
2606
2669
|
"gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
|
|
2607
2670
|
"gitControls.checkFailed": "Git 확인 실패",
|
|
2671
|
+
"gitControls.retryStatus": "다시 확인",
|
|
2608
2672
|
"gitControls.checking": "Git 확인 중",
|
|
2609
2673
|
"gitControls.manageBranchAria": "{branch} branch 관리",
|
|
2610
2674
|
"gitControls.manageBranchTitle": "Branch 변경 및 생성",
|
|
@@ -3653,6 +3717,9 @@ const ko = {
|
|
|
3653
3717
|
"preview.tokenUrlInvalid": "Preview 토큰 URL이 올바르지 않습니다.",
|
|
3654
3718
|
"branchDialog.loading": "Branch 목록을 불러오는 중입니다.",
|
|
3655
3719
|
"branchDialog.empty": "표시할 branch가 없습니다.",
|
|
3720
|
+
"branchDialog.searchLabel": "브랜치 검색",
|
|
3721
|
+
"branchDialog.searchPlaceholder": "브랜치 이름 검색",
|
|
3722
|
+
"branchDialog.searchEmpty": "'{query}'와 일치하는 브랜치가 없습니다.",
|
|
3656
3723
|
"branchDialog.actionsAria": "{name} 브랜치 작업",
|
|
3657
3724
|
"branchDialog.rowAria": "{name} {meta}",
|
|
3658
3725
|
"branchDialog.switch": "브랜치 이동",
|
|
@@ -3667,6 +3734,24 @@ const ko = {
|
|
|
3667
3734
|
"branchDialog.deleteConfirm": "'{name}' 브랜치를 삭제할까요? 머지되지 않은 커밋이 있으면 Git이 거부합니다.",
|
|
3668
3735
|
"branchDialog.deleteConfirmAction": "삭제",
|
|
3669
3736
|
"branchDialog.cancel": "취소",
|
|
3737
|
+
"branchCleanup.openAria": "병합된 브랜치 정리",
|
|
3738
|
+
"branchCleanup.title": "정리 가능한 브랜치",
|
|
3739
|
+
"branchCleanup.loading": "브랜치를 확인하는 중…",
|
|
3740
|
+
"branchCleanup.empty": "정리 가능한 브랜치가 없습니다.",
|
|
3741
|
+
"branchCleanup.scannedNote": "로컬 브랜치 {total}개 중 {count}개가 정리 가능합니다.",
|
|
3742
|
+
"branchCleanup.selectAll": "모두 선택",
|
|
3743
|
+
"branchCleanup.deselectAll": "모두 해제",
|
|
3744
|
+
"branchCleanup.deleteSelected": "{count}개 삭제",
|
|
3745
|
+
"branchCleanup.deleteFailed": "브랜치를 삭제할 수 없습니다.",
|
|
3746
|
+
"branchCleanup.deletePartial": "{total}개 중 {failed}개를 삭제하지 못했습니다: {detail}",
|
|
3747
|
+
"branchCleanup.loadFailed": "브랜치 확인을 실패했습니다.",
|
|
3748
|
+
"branchCleanup.noBaseBranch": "기본 브랜치를 찾을 수 없습니다. origin/HEAD 를 설정하거나 프로젝트 설정에서 기본 브랜치를 지정하세요.",
|
|
3749
|
+
"branchCleanup.probeIncomplete": "브랜치 정보를 완전히 읽지 못했습니다. 목록이 비어 보일 수 있으니 다시 시도하세요.",
|
|
3750
|
+
"branchCleanup.demoUnsupported": "Demo 모드에서는 지원하지 않습니다.",
|
|
3751
|
+
"branchCleanup.refreshAria": "브랜치 목록 새로고침",
|
|
3752
|
+
"branchCleanup.squashScanning": "squash merge된 브랜치를 찾는 중…",
|
|
3753
|
+
"branchCleanup.squashFailed": "squash merge 검사에 실패했습니다. 아래 목록은 일반 merge 후보만입니다.",
|
|
3754
|
+
"branchCleanup.squashTag": "squash 병합",
|
|
3670
3755
|
"message.deleteConfirm": "이 메시지를 삭제할까요? 이 작업은 새로고침 후에도 유지됩니다.",
|
|
3671
3756
|
"message.deleteFailed": "메시지를 삭제할 수 없습니다.",
|
|
3672
3757
|
"message.sendFailed": "메시지를 보낼 수 없습니다.",
|
|
@@ -3955,6 +4040,7 @@ const ko = {
|
|
|
3955
4040
|
"security.patDeleteFailed": "GitHub PAT를 삭제하지 못했습니다.",
|
|
3956
4041
|
// Module-level helper errors (non-hook)
|
|
3957
4042
|
"file.readFailed": "파일을 읽지 못했습니다.",
|
|
4043
|
+
"file.linkOutsideScope": "이 프로젝트가 열 수 있는 폴더 밖의 경로입니다: {path}",
|
|
3958
4044
|
"session.loadMessagesFailed": "대화 메시지를 불러올 수 없습니다.",
|
|
3959
4045
|
"session.sendToSessionFailed": "대화에 메시지를 보낼 수 없습니다.",
|
|
3960
4046
|
"session.sendBlockedAccountKeyLocked": "이 대화는 계정 키로 잠겨 있습니다. 잠금을 해제한 뒤 다시 시도하세요.",
|
|
@@ -7531,6 +7617,7 @@ function parseUnlockMethod(value2) {
|
|
|
7531
7617
|
const kdfParams = parseKdfParams(method.kdfParams);
|
|
7532
7618
|
const salt = boundedAllowEmpty(method.salt, 256);
|
|
7533
7619
|
const wrappedMasterKey = bounded(method.wrappedMasterKey, 16384);
|
|
7620
|
+
const possessionPublicKey = bounded(method.possessionPublicKey, 43);
|
|
7534
7621
|
if (!id || !/^[A-Za-z0-9_-]+$/.test(id) || type !== "password" && type !== "recovery" && type !== "device" || !kdfAlgorithm || !kdfParams || salt === void 0 || !wrappedMasterKey) return void 0;
|
|
7535
7622
|
const revision = method.revision;
|
|
7536
7623
|
if (revision !== void 0 && (!Number.isSafeInteger(revision) || revision < 1)) return void 0;
|
|
@@ -7541,6 +7628,7 @@ function parseUnlockMethod(value2) {
|
|
|
7541
7628
|
kdfParams,
|
|
7542
7629
|
salt,
|
|
7543
7630
|
wrappedMasterKey,
|
|
7631
|
+
...possessionPublicKey && /^[A-Za-z0-9_-]{43}$/.test(possessionPublicKey) ? { possessionPublicKey } : {},
|
|
7544
7632
|
...revision === void 0 ? {} : { revision }
|
|
7545
7633
|
};
|
|
7546
7634
|
}
|
|
@@ -7674,6 +7762,19 @@ async function decryptDataKeyToString(dataEncryptionKey, bundleBase64) {
|
|
|
7674
7762
|
return null;
|
|
7675
7763
|
}
|
|
7676
7764
|
}
|
|
7765
|
+
async function decryptDataKeyJson(dataEncryptionKey, bundleBase64) {
|
|
7766
|
+
const json = await decryptDataKeyToString(dataEncryptionKey, bundleBase64);
|
|
7767
|
+
if (json === null) return null;
|
|
7768
|
+
try {
|
|
7769
|
+
return JSON.parse(json);
|
|
7770
|
+
} catch {
|
|
7771
|
+
return null;
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
async function decryptDataKeyRecord(dataEncryptionKey, bundleBase64) {
|
|
7775
|
+
const value2 = await decryptDataKeyJson(dataEncryptionKey, bundleBase64);
|
|
7776
|
+
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
7777
|
+
}
|
|
7677
7778
|
async function encryptDataKeyContent(dataEncryptionKey, value2) {
|
|
7678
7779
|
const nonce = nacl.randomBytes(GCM_NONCE_LENGTH);
|
|
7679
7780
|
const key = await importGcmKey(dataEncryptionKey);
|
|
@@ -7687,6 +7788,9 @@ async function encryptDataKeyContent(dataEncryptionKey, value2) {
|
|
|
7687
7788
|
}
|
|
7688
7789
|
let candidates = [];
|
|
7689
7790
|
let version = 0;
|
|
7791
|
+
function dataKeyUnwrapCandidates(accountPrivateKey, companyKeys = getCompanyKeyCandidates()) {
|
|
7792
|
+
return accountPrivateKey ? [accountPrivateKey, ...companyKeys] : [];
|
|
7793
|
+
}
|
|
7690
7794
|
function getCompanyKeyCandidates() {
|
|
7691
7795
|
return candidates.map((candidate) => candidate.privateKey);
|
|
7692
7796
|
}
|
|
@@ -7696,9 +7800,25 @@ function companyKeyChainVersion() {
|
|
|
7696
7800
|
const dekBySessionId = /* @__PURE__ */ new Map();
|
|
7697
7801
|
const lockedSessionIds = /* @__PURE__ */ new Set();
|
|
7698
7802
|
const fingerprints = /* @__PURE__ */ new Map();
|
|
7699
|
-
|
|
7803
|
+
const companyUnwrappedEnvelopes = /* @__PURE__ */ new Map();
|
|
7804
|
+
function firstUnwrap(envelope, candidates2) {
|
|
7805
|
+
for (const candidate of candidates2) {
|
|
7806
|
+
const dek = unwrapDataEncryptionKey(envelope, candidate);
|
|
7807
|
+
if (dek) return dek;
|
|
7808
|
+
}
|
|
7809
|
+
return null;
|
|
7810
|
+
}
|
|
7811
|
+
let boundaryGeneration$1 = 0;
|
|
7700
7812
|
function sessionDekRegistryGeneration() {
|
|
7701
|
-
return boundaryGeneration;
|
|
7813
|
+
return boundaryGeneration$1;
|
|
7814
|
+
}
|
|
7815
|
+
function adoptSessionDek(sessionId, envelope, dek, byCompanyKey) {
|
|
7816
|
+
const existing = dekBySessionId.get(sessionId);
|
|
7817
|
+
if (existing && existing.length === dek.length && existing.every((byte, index) => byte === dek[index])) dek.fill(0);
|
|
7818
|
+
else dekBySessionId.set(sessionId, dek);
|
|
7819
|
+
lockedSessionIds.delete(sessionId);
|
|
7820
|
+
if (byCompanyKey) companyUnwrappedEnvelopes.set(sessionId, envelope);
|
|
7821
|
+
else companyUnwrappedEnvelopes.delete(sessionId);
|
|
7702
7822
|
}
|
|
7703
7823
|
function registerSessionDek(session, accountPrivateKey) {
|
|
7704
7824
|
const envelope = session.dataEncryptionKey ?? null;
|
|
@@ -7706,6 +7826,7 @@ function registerSessionDek(session, accountPrivateKey) {
|
|
|
7706
7826
|
dekBySessionId.delete(session.id);
|
|
7707
7827
|
lockedSessionIds.delete(session.id);
|
|
7708
7828
|
fingerprints.delete(session.id);
|
|
7829
|
+
companyUnwrappedEnvelopes.delete(session.id);
|
|
7709
7830
|
return;
|
|
7710
7831
|
}
|
|
7711
7832
|
const fingerprint = `${envelope}|${accountPrivateKey ? "1" : "0"}|${companyKeyChainVersion()}`;
|
|
@@ -7713,20 +7834,16 @@ function registerSessionDek(session, accountPrivateKey) {
|
|
|
7713
7834
|
if (!accountPrivateKey && dekBySessionId.has(session.id) && fingerprints.get(session.id)?.startsWith(`${envelope}|1|`)) return;
|
|
7714
7835
|
const previousFingerprint = fingerprints.get(session.id);
|
|
7715
7836
|
fingerprints.set(session.id, fingerprint);
|
|
7716
|
-
const
|
|
7717
|
-
|
|
7718
|
-
for (const candidate of candidates2) {
|
|
7719
|
-
dek = unwrapDataEncryptionKey(envelope, candidate);
|
|
7720
|
-
if (dek) break;
|
|
7721
|
-
}
|
|
7837
|
+
const accountDek = accountPrivateKey ? unwrapDataEncryptionKey(envelope, accountPrivateKey) : null;
|
|
7838
|
+
const dek = accountDek ?? (accountPrivateKey ? firstUnwrap(envelope, getCompanyKeyCandidates()) : null);
|
|
7722
7839
|
if (dek) {
|
|
7723
|
-
|
|
7724
|
-
lockedSessionIds.delete(session.id);
|
|
7840
|
+
adoptSessionDek(session.id, envelope, dek, accountDek === null);
|
|
7725
7841
|
return;
|
|
7726
7842
|
}
|
|
7727
7843
|
if (dekBySessionId.has(session.id) && previousFingerprint?.startsWith(`${envelope}|`)) return;
|
|
7728
7844
|
dekBySessionId.delete(session.id);
|
|
7729
7845
|
lockedSessionIds.add(session.id);
|
|
7846
|
+
if (companyUnwrappedEnvelopes.has(session.id)) companyUnwrappedEnvelopes.set(session.id, envelope);
|
|
7730
7847
|
}
|
|
7731
7848
|
function getSessionDek(sessionId) {
|
|
7732
7849
|
return dekBySessionId.get(sessionId) ?? null;
|
|
@@ -7768,6 +7885,60 @@ async function preDecryptSessionHeaders(sessions, accountPrivateKey, sinceGenera
|
|
|
7768
7885
|
}
|
|
7769
7886
|
}));
|
|
7770
7887
|
}
|
|
7888
|
+
const machineDekById = /* @__PURE__ */ new Map();
|
|
7889
|
+
let boundaryGeneration = 0;
|
|
7890
|
+
function machineDekRegistryGeneration() {
|
|
7891
|
+
return boundaryGeneration;
|
|
7892
|
+
}
|
|
7893
|
+
function discardMachineDek(machineId) {
|
|
7894
|
+
machineDekById.get(machineId)?.dek.fill(0);
|
|
7895
|
+
machineDekById.delete(machineId);
|
|
7896
|
+
}
|
|
7897
|
+
function machineFingerprint(envelope) {
|
|
7898
|
+
return `${envelope}|${companyKeyChainVersion()}`;
|
|
7899
|
+
}
|
|
7900
|
+
async function resolveMachineDataKeyFields(record2, legacyKeys, privateKeys, scope) {
|
|
7901
|
+
const discard = { machineId: record2.id, fields: null, entry: null };
|
|
7902
|
+
if (!record2.dataEncryptionKey || !record2.metadata) return discard;
|
|
7903
|
+
if (decryptJsonWithCandidates(record2.metadata, legacyKeys)) return discard;
|
|
7904
|
+
const fingerprint = machineFingerprint(record2.dataEncryptionKey);
|
|
7905
|
+
const cached = machineDekById.get(record2.id);
|
|
7906
|
+
const machineKey = cached?.fingerprint === fingerprint ? cached.dek : unwrapMachineKey(record2.dataEncryptionKey, privateKeys);
|
|
7907
|
+
if (!machineKey) return discard;
|
|
7908
|
+
const metadata = await decryptDataKeyRecord(machineKey, record2.metadata);
|
|
7909
|
+
if (!metadata) return discard;
|
|
7910
|
+
const daemonState = record2.daemonState ? await decryptDataKeyRecord(machineKey, record2.daemonState) : null;
|
|
7911
|
+
const entry = cached?.dek === machineKey ? cached : { dek: machineKey, scope, fingerprint };
|
|
7912
|
+
entry.scope = scope;
|
|
7913
|
+
return { machineId: record2.id, fields: { metadata, daemonState }, entry };
|
|
7914
|
+
}
|
|
7915
|
+
function commitMachineDek(resolved) {
|
|
7916
|
+
if (resolved.entry) machineDekById.set(resolved.machineId, resolved.entry);
|
|
7917
|
+
else discardMachineDek(resolved.machineId);
|
|
7918
|
+
}
|
|
7919
|
+
function unwrapMachineKey(envelope, privateKeys) {
|
|
7920
|
+
for (const privateKey of privateKeys) {
|
|
7921
|
+
const machineKey = unwrapDataEncryptionKey(envelope, privateKey);
|
|
7922
|
+
if (machineKey) return machineKey;
|
|
7923
|
+
}
|
|
7924
|
+
return null;
|
|
7925
|
+
}
|
|
7926
|
+
async function decryptMachineDataKeyRows(rows, legacyKeys, options, scope, sinceGeneration = machineDekRegistryGeneration()) {
|
|
7927
|
+
const none = () => rows.map(() => null);
|
|
7928
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7929
|
+
const listed = new Set(rows.map((row2) => row2.id));
|
|
7930
|
+
for (const [machineId, entry] of [...machineDekById]) {
|
|
7931
|
+
if (entry.scope === scope && !listed.has(machineId)) discardMachineDek(machineId);
|
|
7932
|
+
}
|
|
7933
|
+
if (!options.accountPrivateKey || !rows.some((row2) => row2.dataEncryptionKey)) return none();
|
|
7934
|
+
await options.dataKeyReady;
|
|
7935
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7936
|
+
const privateKeys = dataKeyUnwrapCandidates(options.accountPrivateKey);
|
|
7937
|
+
const resolved = await Promise.all(rows.map((row2) => resolveMachineDataKeyFields(row2, legacyKeys, privateKeys, scope)));
|
|
7938
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7939
|
+
for (const entry of resolved) commitMachineDek(entry);
|
|
7940
|
+
return resolved.map((entry) => entry.fields);
|
|
7941
|
+
}
|
|
7771
7942
|
function buildUserTextMessageContent(input) {
|
|
7772
7943
|
return {
|
|
7773
7944
|
role: "user",
|
|
@@ -8678,6 +8849,53 @@ function sessionFilesystemScopePatch(metadata, existing) {
|
|
|
8678
8849
|
}
|
|
8679
8850
|
return { filesystemScope: parseSessionFilesystemScope(metadata.filesystemScope) };
|
|
8680
8851
|
}
|
|
8852
|
+
const claudeEffortOptions = [
|
|
8853
|
+
{ key: "low", label: "Low" },
|
|
8854
|
+
{ key: "medium", label: "Medium" },
|
|
8855
|
+
{ key: "high", label: "High" },
|
|
8856
|
+
{ key: "max", label: "Max" }
|
|
8857
|
+
];
|
|
8858
|
+
const claudeFableEffortOptions = [
|
|
8859
|
+
{ key: "low", label: "Low" },
|
|
8860
|
+
{ key: "medium", label: "Medium" },
|
|
8861
|
+
{ key: "high", label: "High" },
|
|
8862
|
+
{ key: "xhigh", label: "Xhigh" },
|
|
8863
|
+
{ key: "max", label: "Max" }
|
|
8864
|
+
];
|
|
8865
|
+
const codexEffortOptions = [
|
|
8866
|
+
{ key: "low", label: "Low" },
|
|
8867
|
+
{ key: "medium", label: "Medium" },
|
|
8868
|
+
{ key: "high", label: "High" },
|
|
8869
|
+
{ key: "xhigh", label: "Xhigh" }
|
|
8870
|
+
];
|
|
8871
|
+
const codexMaxEffortOptions = [
|
|
8872
|
+
...codexEffortOptions,
|
|
8873
|
+
{ key: "max", label: "Max" }
|
|
8874
|
+
];
|
|
8875
|
+
const codexUltraEffortOptions = [
|
|
8876
|
+
...codexMaxEffortOptions,
|
|
8877
|
+
{ key: "ultra", label: "Ultra" }
|
|
8878
|
+
];
|
|
8879
|
+
function getEffortOptionsForAgent(agent, model) {
|
|
8880
|
+
if (model === "default") return [];
|
|
8881
|
+
if (agent === "codex") {
|
|
8882
|
+
if (model === "gpt-6-astra" || model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
|
|
8883
|
+
if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
|
|
8884
|
+
return codexEffortOptions;
|
|
8885
|
+
}
|
|
8886
|
+
if (agent === "gemini" || agent === "opencode" || agent === "grok") return [];
|
|
8887
|
+
if (model === "claude-fable-5-1") return claudeFableEffortOptions;
|
|
8888
|
+
return claudeEffortOptions;
|
|
8889
|
+
}
|
|
8890
|
+
function defaultEffortForAgent(agent, model) {
|
|
8891
|
+
const options = getEffortOptionsForAgent(agent, model);
|
|
8892
|
+
if (options.length === 0) return null;
|
|
8893
|
+
if (agent === "codex") {
|
|
8894
|
+
if (model === "gpt-5.6-sol") return "low";
|
|
8895
|
+
if (model === "gpt-6-astra" || model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
|
|
8896
|
+
}
|
|
8897
|
+
return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
|
|
8898
|
+
}
|
|
8681
8899
|
function normalizeEpochMilliseconds(value2) {
|
|
8682
8900
|
const abs = Math.abs(value2);
|
|
8683
8901
|
if (abs >= 1e15) return value2 / 1e3;
|
|
@@ -17951,10 +18169,10 @@ function normalizeAutomationSupport(value2) {
|
|
|
17951
18169
|
...Number.isSafeInteger(capability.keyVersion) && capability.keyVersion > 0 ? { keyVersion: capability.keyVersion } : {}
|
|
17952
18170
|
};
|
|
17953
18171
|
}
|
|
17954
|
-
function normalizeMachine(row2, candidateKeys) {
|
|
18172
|
+
function normalizeMachine(row2, candidateKeys, dataKeyFields = null) {
|
|
17955
18173
|
const registry = row2.registry;
|
|
17956
|
-
const decrypted = decryptJsonWithCandidates(row2.metadata, candidateKeys) ?? {};
|
|
17957
|
-
const daemonState = decryptJsonWithCandidates(row2.daemonState, candidateKeys);
|
|
18174
|
+
const decrypted = dataKeyFields?.metadata ?? decryptJsonWithCandidates(row2.metadata, candidateKeys) ?? {};
|
|
18175
|
+
const daemonState = dataKeyFields?.daemonState ?? decryptJsonWithCandidates(row2.daemonState, candidateKeys);
|
|
17958
18176
|
const resources = row2.resources ?? parseMachineResources(daemonState);
|
|
17959
18177
|
const host = row2.host || registry?.host || (typeof decrypted.host === "string" ? decrypted.host : row2.id.slice(0, 8));
|
|
17960
18178
|
const platform2 = row2.platform || registry?.platform || (typeof decrypted.platform === "string" ? decrypted.platform : "unknown");
|
|
@@ -19180,13 +19398,16 @@ class ApiClient {
|
|
|
19180
19398
|
async getMachines(companyId, candidateKeys = [], options = {}) {
|
|
19181
19399
|
const company = companyId ?? "personal";
|
|
19182
19400
|
const statusParam = options.withStatus ? "&withStatus=1" : "";
|
|
19401
|
+
const dekGeneration = machineDekRegistryGeneration();
|
|
19183
19402
|
const data = await this.request(
|
|
19184
19403
|
`/api/machines/list?company=${encodeURIComponent(company)}${statusParam}`,
|
|
19185
19404
|
{ boundary: "api" }
|
|
19186
19405
|
);
|
|
19187
19406
|
const rows = Array.isArray(data) ? data : data.machines ?? [];
|
|
19188
19407
|
const registry = await this.fetchMachineRegistry(rows.map((row2) => row2.id));
|
|
19189
|
-
|
|
19408
|
+
const enriched = rows.map((row2) => withMachineRegistry(row2, registry));
|
|
19409
|
+
const dataKeyFields = await decryptMachineDataKeyRows(enriched, candidateKeys, options, company, dekGeneration);
|
|
19410
|
+
return enriched.map((row2, index) => normalizeMachine(row2, candidateKeys, dataKeyFields[index]));
|
|
19190
19411
|
}
|
|
19191
19412
|
/**
|
|
19192
19413
|
* Fail-open: a registry outage degrades machine names back to the daemon
|
|
@@ -21569,53 +21790,6 @@ function chatStartupHints(supportsQueuedInput = false) {
|
|
|
21569
21790
|
"At the prompt: /model · /effort · /voice to dictate · /detach to leave running · /exit to leave.\n"
|
|
21570
21791
|
];
|
|
21571
21792
|
}
|
|
21572
|
-
const claudeEffortOptions = [
|
|
21573
|
-
{ key: "low", label: "Low" },
|
|
21574
|
-
{ key: "medium", label: "Medium" },
|
|
21575
|
-
{ key: "high", label: "High" },
|
|
21576
|
-
{ key: "max", label: "Max" }
|
|
21577
|
-
];
|
|
21578
|
-
const claudeFableEffortOptions = [
|
|
21579
|
-
{ key: "low", label: "Low" },
|
|
21580
|
-
{ key: "medium", label: "Medium" },
|
|
21581
|
-
{ key: "high", label: "High" },
|
|
21582
|
-
{ key: "xhigh", label: "Xhigh" },
|
|
21583
|
-
{ key: "max", label: "Max" }
|
|
21584
|
-
];
|
|
21585
|
-
const codexEffortOptions = [
|
|
21586
|
-
{ key: "low", label: "Low" },
|
|
21587
|
-
{ key: "medium", label: "Medium" },
|
|
21588
|
-
{ key: "high", label: "High" },
|
|
21589
|
-
{ key: "xhigh", label: "Xhigh" }
|
|
21590
|
-
];
|
|
21591
|
-
const codexMaxEffortOptions = [
|
|
21592
|
-
...codexEffortOptions,
|
|
21593
|
-
{ key: "max", label: "Max" }
|
|
21594
|
-
];
|
|
21595
|
-
const codexUltraEffortOptions = [
|
|
21596
|
-
...codexMaxEffortOptions,
|
|
21597
|
-
{ key: "ultra", label: "Ultra" }
|
|
21598
|
-
];
|
|
21599
|
-
function getEffortOptionsForAgent(agent, model) {
|
|
21600
|
-
if (model === "default") return [];
|
|
21601
|
-
if (agent === "codex") {
|
|
21602
|
-
if (model === "gpt-6-astra" || model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
|
|
21603
|
-
if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
|
|
21604
|
-
return codexEffortOptions;
|
|
21605
|
-
}
|
|
21606
|
-
if (agent === "gemini" || agent === "opencode" || agent === "grok") return [];
|
|
21607
|
-
if (model === "claude-fable-5-1") return claudeFableEffortOptions;
|
|
21608
|
-
return claudeEffortOptions;
|
|
21609
|
-
}
|
|
21610
|
-
function defaultEffortForAgent(agent, model) {
|
|
21611
|
-
const options = getEffortOptionsForAgent(agent, model);
|
|
21612
|
-
if (options.length === 0) return null;
|
|
21613
|
-
if (agent === "codex") {
|
|
21614
|
-
if (model === "gpt-5.6-sol") return "low";
|
|
21615
|
-
if (model === "gpt-6-astra" || model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
|
|
21616
|
-
}
|
|
21617
|
-
return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
|
|
21618
|
-
}
|
|
21619
21793
|
const newSessionAgents = [
|
|
21620
21794
|
{ id: "claude", label: "Claude", provider: "Anthropic" },
|
|
21621
21795
|
{ id: "codex", label: "Codex", provider: "OpenAI" },
|
|
@@ -21754,8 +21928,8 @@ function buildLiveChatUserMessage(text2, settings) {
|
|
|
21754
21928
|
return {
|
|
21755
21929
|
role: "user",
|
|
21756
21930
|
content: { type: "text", text: text2 },
|
|
21931
|
+
// permissionMode 는 싣지 않는다 — 부재가 곧 "현재 모드 유지" (specs/delegated-session-permission-mode).
|
|
21757
21932
|
meta: {
|
|
21758
|
-
permissionMode: "default",
|
|
21759
21933
|
...modelOptions.model ? { model: modelOptions.model } : {},
|
|
21760
21934
|
...modelOptions.effort ? { effort: modelOptions.effort } : {}
|
|
21761
21935
|
}
|
|
@@ -21813,6 +21987,141 @@ function resumeHistoryCount(input) {
|
|
|
21813
21987
|
if (!input.resuming) return 0;
|
|
21814
21988
|
return input.since ?? DEFAULT_RESUME_HISTORY;
|
|
21815
21989
|
}
|
|
21990
|
+
const AGENT_SPAWN_BUDGET_MIN = 4;
|
|
21991
|
+
const AGENT_SPAWN_BUDGET_MAX = 32;
|
|
21992
|
+
const MEMORY_BYTES_PER_AGENT = 2 * 1024 ** 3;
|
|
21993
|
+
const AGENTS_PER_CORE = 2;
|
|
21994
|
+
function machineSpawnBudget(capacity) {
|
|
21995
|
+
const cores = Number.isFinite(capacity.cpuCount) ? Math.max(0, Math.floor(capacity.cpuCount)) : 0;
|
|
21996
|
+
const memory = Number.isFinite(capacity.totalMemoryBytes) ? Math.max(0, capacity.totalMemoryBytes) : 0;
|
|
21997
|
+
if (cores <= 0 || memory <= 0) return AGENT_SPAWN_BUDGET_MIN;
|
|
21998
|
+
const budget = Math.min(Math.floor(memory / MEMORY_BYTES_PER_AGENT), cores * AGENTS_PER_CORE);
|
|
21999
|
+
return Math.min(Math.max(budget, AGENT_SPAWN_BUDGET_MIN), AGENT_SPAWN_BUDGET_MAX);
|
|
22000
|
+
}
|
|
22001
|
+
function tierSpawnBudget(depth, machineBudget) {
|
|
22002
|
+
return depth <= 0 ? machineBudget : Math.ceil(machineBudget / 2);
|
|
22003
|
+
}
|
|
22004
|
+
function resolveMaxSpawn(input) {
|
|
22005
|
+
const tier = tierSpawnBudget(input.depth, input.machineBudget);
|
|
22006
|
+
return input.policy === null ? tier : Math.min(input.policy, tier);
|
|
22007
|
+
}
|
|
22008
|
+
const SPAWN_BUDGET_WINDOW_MS = 6 * 60 * 60 * 1e3;
|
|
22009
|
+
function isCharged(chargedAt, now) {
|
|
22010
|
+
return chargedAt > now - SPAWN_BUDGET_WINDOW_MS;
|
|
22011
|
+
}
|
|
22012
|
+
function chargeTimes(records) {
|
|
22013
|
+
const byId = /* @__PURE__ */ new Map();
|
|
22014
|
+
const unkeyed = [];
|
|
22015
|
+
for (const record2 of records) {
|
|
22016
|
+
if (record2.spawnId === void 0) {
|
|
22017
|
+
unkeyed.push(record2.ts);
|
|
22018
|
+
continue;
|
|
22019
|
+
}
|
|
22020
|
+
const seen = byId.get(record2.spawnId);
|
|
22021
|
+
if (seen === void 0 || record2.ts < seen) byId.set(record2.spawnId, record2.ts);
|
|
22022
|
+
}
|
|
22023
|
+
return [...byId.values(), ...unkeyed];
|
|
22024
|
+
}
|
|
22025
|
+
function chargedSpawnCount(records, now) {
|
|
22026
|
+
return chargeTimes(records).filter((chargedAt) => isCharged(chargedAt, now)).length;
|
|
22027
|
+
}
|
|
22028
|
+
function spawnBudgetResetsAt(records, now, maxSpawn) {
|
|
22029
|
+
if (maxSpawn <= 0) return null;
|
|
22030
|
+
const expiries = chargeTimes(records).filter((chargedAt) => isCharged(chargedAt, now)).map((ts) => ts + SPAWN_BUDGET_WINDOW_MS).sort((a, b) => a - b);
|
|
22031
|
+
if (expiries.length < maxSpawn) return null;
|
|
22032
|
+
return expiries[expiries.length - maxSpawn];
|
|
22033
|
+
}
|
|
22034
|
+
function spawnLimitDetail(input) {
|
|
22035
|
+
const used = `${input.spawned}/${input.maxSpawn} used`;
|
|
22036
|
+
if (input.resetsAt === null || input.resetsAt <= input.now) return used;
|
|
22037
|
+
const minutes = Math.ceil((input.resetsAt - input.now) / 6e4);
|
|
22038
|
+
return `${used}; the next slot frees in ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
22039
|
+
}
|
|
22040
|
+
const AGENT_ENV_FLAG = "SAYCODE_AGENT_ENV";
|
|
22041
|
+
const AGENT_MAX_DEPTH = 2;
|
|
22042
|
+
function readOptionalNonNegativeInt(raw) {
|
|
22043
|
+
if (raw === void 0 || !/^\d+$/.test(raw.trim())) return null;
|
|
22044
|
+
const value2 = Number(raw.trim());
|
|
22045
|
+
return Number.isSafeInteger(value2) ? value2 : null;
|
|
22046
|
+
}
|
|
22047
|
+
function normalizeDirectory(raw) {
|
|
22048
|
+
const trimmed = raw.trim();
|
|
22049
|
+
if (!trimmed) return "";
|
|
22050
|
+
const stripped = trimmed.replace(/\/+$/, "");
|
|
22051
|
+
return stripped || "/";
|
|
22052
|
+
}
|
|
22053
|
+
function hasAgentMarker(env) {
|
|
22054
|
+
return env[AGENT_ENV_FLAG] === "1";
|
|
22055
|
+
}
|
|
22056
|
+
function isAgentEnv(env) {
|
|
22057
|
+
return hasAgentMarker(env) && normalizeDirectory(env.SAYCODE_AGENT_ROOT ?? "") !== "";
|
|
22058
|
+
}
|
|
22059
|
+
function readAgentEnv(env, machineBudget) {
|
|
22060
|
+
if (!isAgentEnv(env)) return null;
|
|
22061
|
+
const root = normalizeDirectory(env.SAYCODE_AGENT_ROOT ?? "");
|
|
22062
|
+
const depth = readOptionalNonNegativeInt(env.SAYCODE_AGENT_DEPTH) ?? 0;
|
|
22063
|
+
return {
|
|
22064
|
+
root,
|
|
22065
|
+
scope: normalizeDirectory(env.SAYCODE_AGENT_SCOPE ?? "") || root,
|
|
22066
|
+
depth,
|
|
22067
|
+
maxSpawn: resolveMaxSpawn({
|
|
22068
|
+
policy: readOptionalNonNegativeInt(env.SAYCODE_AGENT_MAX_SPAWN),
|
|
22069
|
+
depth,
|
|
22070
|
+
machineBudget
|
|
22071
|
+
}),
|
|
22072
|
+
agentId: env.SAYCODE_AGENT_ID?.trim() || null,
|
|
22073
|
+
sessionId: env.APLUS_SESSION_ID?.trim() || null
|
|
22074
|
+
};
|
|
22075
|
+
}
|
|
22076
|
+
function spawnDecision(input) {
|
|
22077
|
+
if (input.depth >= AGENT_MAX_DEPTH) return "spawn_depth_exceeded";
|
|
22078
|
+
if (input.spawned >= input.maxSpawn) return "spawn_limit_exceeded";
|
|
22079
|
+
return "allow";
|
|
22080
|
+
}
|
|
22081
|
+
function childAgentEnv(parent, childAgentId) {
|
|
22082
|
+
return {
|
|
22083
|
+
[AGENT_ENV_FLAG]: "1",
|
|
22084
|
+
SAYCODE_AGENT_ROOT: parent.root,
|
|
22085
|
+
SAYCODE_AGENT_SCOPE: parent.scope,
|
|
22086
|
+
SAYCODE_AGENT_DEPTH: String(parent.depth + 1),
|
|
22087
|
+
SAYCODE_AGENT_MAX_SPAWN: String(tierSpawnBudget(parent.depth + 1, parent.maxSpawn)),
|
|
22088
|
+
SAYCODE_AGENT_ID: childAgentId
|
|
22089
|
+
};
|
|
22090
|
+
}
|
|
22091
|
+
function isInScope(session, scope) {
|
|
22092
|
+
if (!session.machineId || !scope.machineId) return false;
|
|
22093
|
+
if (session.machineId !== scope.machineId) return false;
|
|
22094
|
+
const root = normalizeDirectory(scope.root);
|
|
22095
|
+
const directory = normalizeDirectory(session.directory);
|
|
22096
|
+
if (!root || !directory) return false;
|
|
22097
|
+
if (directory === root) return true;
|
|
22098
|
+
return directory.startsWith(root === "/" ? "/" : `${root}/`);
|
|
22099
|
+
}
|
|
22100
|
+
function sessionsInScope(sessions, scope) {
|
|
22101
|
+
return sessions.filter((session) => isInScope(session, scope));
|
|
22102
|
+
}
|
|
22103
|
+
function machineSettingsPaths(input) {
|
|
22104
|
+
const override = input.env.SAYCODE_HAPPY_HOME?.trim();
|
|
22105
|
+
const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
|
|
22106
|
+
const homes = CREDENTIAL_SOURCE_ORDER.filter((source) => source !== "saycode").map((source) => `${input.homeDir}/.${source}`);
|
|
22107
|
+
const ordered = [input.credentialHome?.trim(), override, runtimeHome, ...homes];
|
|
22108
|
+
return [...new Set(ordered.filter((home) => Boolean(home)))].map((home) => `${home}/settings.json`.replace(/\/{2,}/g, "/"));
|
|
22109
|
+
}
|
|
22110
|
+
function readMachineId(input, readFile2) {
|
|
22111
|
+
for (const path of machineSettingsPaths(input)) {
|
|
22112
|
+
const raw = readFile2(path);
|
|
22113
|
+
if (!raw) continue;
|
|
22114
|
+
try {
|
|
22115
|
+
const parsed = JSON.parse(raw);
|
|
22116
|
+
if (typeof parsed?.machineId === "string" && parsed.machineId) return parsed.machineId;
|
|
22117
|
+
} catch {
|
|
22118
|
+
}
|
|
22119
|
+
}
|
|
22120
|
+
return null;
|
|
22121
|
+
}
|
|
22122
|
+
function spawnBudgetKey(env) {
|
|
22123
|
+
return env.agentId ?? env.root;
|
|
22124
|
+
}
|
|
21816
22125
|
const REAL_CLI_SESSION_CONNECTION_DEPS = {
|
|
21817
22126
|
createLocalId: () => `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`
|
|
21818
22127
|
};
|
|
@@ -23891,6 +24200,9 @@ async function askTemporaryLine(question) {
|
|
|
23891
24200
|
}
|
|
23892
24201
|
async function runChat(context, options, providedTransport) {
|
|
23893
24202
|
const resuming = !!options.sessionId;
|
|
24203
|
+
if (!resuming && hasAgentMarker(process.env)) {
|
|
24204
|
+
throw new CliError("This session may not start a new conversation with `chat`. Use `saycode agent spawn`, which applies the spawn budget and hands the child its lineage. `chat <session-id>` to attach still works.");
|
|
24205
|
+
}
|
|
23894
24206
|
let optionPrompter = null;
|
|
23895
24207
|
let optionInput = null;
|
|
23896
24208
|
if (!resuming && options.promptForOptions) {
|
|
@@ -24795,6 +25107,9 @@ function waitForSessionState(options) {
|
|
|
24795
25107
|
});
|
|
24796
25108
|
}
|
|
24797
25109
|
const AGENT_FAILURE_EXIT_CODE = 3;
|
|
25110
|
+
function isAgentFailure(value2) {
|
|
25111
|
+
return typeof value2 === "object" && value2 !== null && "stdout" in value2;
|
|
25112
|
+
}
|
|
24798
25113
|
function line(value2) {
|
|
24799
25114
|
return `${JSON.stringify(value2)}
|
|
24800
25115
|
`;
|
|
@@ -24927,87 +25242,6 @@ async function promptTarget(runtime, target, text2, options) {
|
|
|
24927
25242
|
stream2.close();
|
|
24928
25243
|
}
|
|
24929
25244
|
}
|
|
24930
|
-
const AGENT_ENV_FLAG = "SAYCODE_AGENT_ENV";
|
|
24931
|
-
const AGENT_MAX_DEPTH = 2;
|
|
24932
|
-
const DEFAULT_AGENT_MAX_SPAWN = 4;
|
|
24933
|
-
const AGENT_MAX_SPAWN_CEILING = 16;
|
|
24934
|
-
function readNonNegativeInt(raw, fallback) {
|
|
24935
|
-
if (raw === void 0) return fallback;
|
|
24936
|
-
if (!/^\d+$/.test(raw.trim())) return fallback;
|
|
24937
|
-
const value2 = Number(raw.trim());
|
|
24938
|
-
return Number.isSafeInteger(value2) ? value2 : fallback;
|
|
24939
|
-
}
|
|
24940
|
-
function normalizeDirectory(raw) {
|
|
24941
|
-
const trimmed = raw.trim();
|
|
24942
|
-
if (!trimmed) return "";
|
|
24943
|
-
const stripped = trimmed.replace(/\/+$/, "");
|
|
24944
|
-
return stripped || "/";
|
|
24945
|
-
}
|
|
24946
|
-
function readAgentEnv(env) {
|
|
24947
|
-
if (env[AGENT_ENV_FLAG] !== "1") return null;
|
|
24948
|
-
const root = normalizeDirectory(env.SAYCODE_AGENT_ROOT ?? "");
|
|
24949
|
-
if (!root) return null;
|
|
24950
|
-
return {
|
|
24951
|
-
root,
|
|
24952
|
-
scope: normalizeDirectory(env.SAYCODE_AGENT_SCOPE ?? "") || root,
|
|
24953
|
-
depth: readNonNegativeInt(env.SAYCODE_AGENT_DEPTH, 0),
|
|
24954
|
-
maxSpawn: Math.min(
|
|
24955
|
-
readNonNegativeInt(env.SAYCODE_AGENT_MAX_SPAWN, DEFAULT_AGENT_MAX_SPAWN),
|
|
24956
|
-
AGENT_MAX_SPAWN_CEILING
|
|
24957
|
-
),
|
|
24958
|
-
agentId: env.SAYCODE_AGENT_ID?.trim() || null,
|
|
24959
|
-
sessionId: env.APLUS_SESSION_ID?.trim() || null
|
|
24960
|
-
};
|
|
24961
|
-
}
|
|
24962
|
-
function spawnDecision(input) {
|
|
24963
|
-
if (input.depth >= AGENT_MAX_DEPTH) return "spawn_depth_exceeded";
|
|
24964
|
-
if (input.spawned >= input.maxSpawn) return "spawn_limit_exceeded";
|
|
24965
|
-
return "allow";
|
|
24966
|
-
}
|
|
24967
|
-
function childAgentEnv(parent, childAgentId) {
|
|
24968
|
-
return {
|
|
24969
|
-
[AGENT_ENV_FLAG]: "1",
|
|
24970
|
-
SAYCODE_AGENT_ROOT: parent.root,
|
|
24971
|
-
SAYCODE_AGENT_SCOPE: parent.scope,
|
|
24972
|
-
SAYCODE_AGENT_DEPTH: String(parent.depth + 1),
|
|
24973
|
-
SAYCODE_AGENT_MAX_SPAWN: String(Math.min(parent.maxSpawn, DEFAULT_AGENT_MAX_SPAWN)),
|
|
24974
|
-
SAYCODE_AGENT_ID: childAgentId
|
|
24975
|
-
};
|
|
24976
|
-
}
|
|
24977
|
-
function isInScope(session, scope) {
|
|
24978
|
-
if (!session.machineId || !scope.machineId) return false;
|
|
24979
|
-
if (session.machineId !== scope.machineId) return false;
|
|
24980
|
-
const root = normalizeDirectory(scope.root);
|
|
24981
|
-
const directory = normalizeDirectory(session.directory);
|
|
24982
|
-
if (!root || !directory) return false;
|
|
24983
|
-
if (directory === root) return true;
|
|
24984
|
-
return directory.startsWith(root === "/" ? "/" : `${root}/`);
|
|
24985
|
-
}
|
|
24986
|
-
function sessionsInScope(sessions, scope) {
|
|
24987
|
-
return sessions.filter((session) => isInScope(session, scope));
|
|
24988
|
-
}
|
|
24989
|
-
function machineSettingsPaths(input) {
|
|
24990
|
-
const override = input.env.SAYCODE_HAPPY_HOME?.trim();
|
|
24991
|
-
const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
|
|
24992
|
-
const homes = CREDENTIAL_SOURCE_ORDER.filter((source) => source !== "saycode").map((source) => `${input.homeDir}/.${source}`);
|
|
24993
|
-
const ordered = [input.credentialHome?.trim(), override, runtimeHome, ...homes];
|
|
24994
|
-
return [...new Set(ordered.filter((home) => Boolean(home)))].map((home) => `${home}/settings.json`.replace(/\/{2,}/g, "/"));
|
|
24995
|
-
}
|
|
24996
|
-
function readMachineId(input, readFile2) {
|
|
24997
|
-
for (const path of machineSettingsPaths(input)) {
|
|
24998
|
-
const raw = readFile2(path);
|
|
24999
|
-
if (!raw) continue;
|
|
25000
|
-
try {
|
|
25001
|
-
const parsed = JSON.parse(raw);
|
|
25002
|
-
if (typeof parsed?.machineId === "string" && parsed.machineId) return parsed.machineId;
|
|
25003
|
-
} catch {
|
|
25004
|
-
}
|
|
25005
|
-
}
|
|
25006
|
-
return null;
|
|
25007
|
-
}
|
|
25008
|
-
function spawnBudgetKey(env) {
|
|
25009
|
-
return env.agentId ?? env.root;
|
|
25010
|
-
}
|
|
25011
25245
|
function normalizeAbsolutePath(raw) {
|
|
25012
25246
|
const trimmed = raw.trim();
|
|
25013
25247
|
if (!trimmed.startsWith("/")) return "";
|
|
@@ -25118,6 +25352,71 @@ function buildAgentHandoffPrompt(source, context) {
|
|
|
25118
25352
|
...FIRST_ACTIONS.map((action) => `- ${action}`)
|
|
25119
25353
|
].join("\n");
|
|
25120
25354
|
}
|
|
25355
|
+
function checkSpawnBudget(runtime, env) {
|
|
25356
|
+
const key = spawnBudgetKey(env);
|
|
25357
|
+
let usage;
|
|
25358
|
+
try {
|
|
25359
|
+
usage = runtime.countSpawns(key, env.maxSpawn);
|
|
25360
|
+
} catch (error) {
|
|
25361
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger is unreadable, refusing: ${String(error)}`);
|
|
25362
|
+
}
|
|
25363
|
+
const decision = spawnDecision({ depth: env.depth, maxSpawn: env.maxSpawn, spawned: usage.spawned });
|
|
25364
|
+
if (decision === "allow") return { spawned: usage.spawned, key };
|
|
25365
|
+
return agentFail(decision, spawnLimitDetail({
|
|
25366
|
+
spawned: usage.spawned,
|
|
25367
|
+
maxSpawn: env.maxSpawn,
|
|
25368
|
+
resetsAt: usage.resetsAt,
|
|
25369
|
+
now: runtime.now()
|
|
25370
|
+
}));
|
|
25371
|
+
}
|
|
25372
|
+
function reserveSpawn(runtime, env, key, attempt2, spawnedBefore) {
|
|
25373
|
+
const spawnId = runtime.newAgentId();
|
|
25374
|
+
try {
|
|
25375
|
+
runtime.recordSpawn({
|
|
25376
|
+
ts: runtime.now(),
|
|
25377
|
+
agentKey: key,
|
|
25378
|
+
root: env.scope,
|
|
25379
|
+
depth: env.depth,
|
|
25380
|
+
// Unknown until the daemon answers; `resolveSpawn` appends the id under
|
|
25381
|
+
// the same `spawnId`, and the counter charges the pair once.
|
|
25382
|
+
childSessionId: null,
|
|
25383
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25384
|
+
agent: attempt2.agent,
|
|
25385
|
+
promptPreview: attempt2.promptPreview,
|
|
25386
|
+
outcome: "failed",
|
|
25387
|
+
spawnId
|
|
25388
|
+
});
|
|
25389
|
+
} catch (error) {
|
|
25390
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger is unwritable, refusing before anything is created: ${String(error)}`);
|
|
25391
|
+
}
|
|
25392
|
+
try {
|
|
25393
|
+
if (runtime.countSpawns(key, env.maxSpawn).spawned <= spawnedBefore) {
|
|
25394
|
+
return agentFail("spawn_limit_exceeded", "the spawn reservation did not land in the ledger, refusing before anything is created");
|
|
25395
|
+
}
|
|
25396
|
+
} catch (error) {
|
|
25397
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger became unreadable after reserving, refusing: ${String(error)}`);
|
|
25398
|
+
}
|
|
25399
|
+
return { spawnId };
|
|
25400
|
+
}
|
|
25401
|
+
function resolveSpawn(runtime, env, key, spawnId, outcome) {
|
|
25402
|
+
try {
|
|
25403
|
+
runtime.recordSpawn({
|
|
25404
|
+
ts: runtime.now(),
|
|
25405
|
+
agentKey: key,
|
|
25406
|
+
root: env.scope,
|
|
25407
|
+
depth: env.depth,
|
|
25408
|
+
childSessionId: outcome.sessionId,
|
|
25409
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25410
|
+
agent: outcome.agent,
|
|
25411
|
+
promptPreview: outcome.promptPreview,
|
|
25412
|
+
outcome: outcome.sessionId ? "spawned" : "failed",
|
|
25413
|
+
spawnId
|
|
25414
|
+
});
|
|
25415
|
+
return true;
|
|
25416
|
+
} catch {
|
|
25417
|
+
return false;
|
|
25418
|
+
}
|
|
25419
|
+
}
|
|
25121
25420
|
async function fileSpawnedSessionUnderProject(runtime, sessionId, directory) {
|
|
25122
25421
|
if (!runtime.registerSpawnedSession) return;
|
|
25123
25422
|
try {
|
|
@@ -25163,15 +25462,9 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25163
25462
|
if (env.depth >= AGENT_MAX_DEPTH) {
|
|
25164
25463
|
return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
|
|
25165
25464
|
}
|
|
25166
|
-
const
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
spawned = runtime.countSpawns(key);
|
|
25170
|
-
} catch (error) {
|
|
25171
|
-
return agentFail("spawn_limit_exceeded", `spawn ledger is unreadable, refusing: ${String(error)}`);
|
|
25172
|
-
}
|
|
25173
|
-
const decision = spawnDecision({ depth: env.depth, maxSpawn: env.maxSpawn, spawned });
|
|
25174
|
-
if (decision !== "allow") return agentFail(decision, `${spawned}/${env.maxSpawn} used`);
|
|
25465
|
+
const budget = checkSpawnBudget(runtime, env);
|
|
25466
|
+
if (isAgentFailure(budget)) return budget;
|
|
25467
|
+
const { spawned, key } = budget;
|
|
25175
25468
|
let prompt;
|
|
25176
25469
|
try {
|
|
25177
25470
|
prompt = buildHandoffPrompt(await runtime.readMessages(from, HANDOFF_READ_MESSAGES), source, agent);
|
|
@@ -25179,6 +25472,9 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25179
25472
|
return agentFail("handoff_context_failed", String(error));
|
|
25180
25473
|
}
|
|
25181
25474
|
const childId = runtime.newAgentId();
|
|
25475
|
+
const preview = `handoff from ${from}`;
|
|
25476
|
+
const reservation = reserveSpawn(runtime, env, key, { agent, promptPreview: preview }, spawned);
|
|
25477
|
+
if (isAgentFailure(reservation)) return reservation;
|
|
25182
25478
|
let sessionId = null;
|
|
25183
25479
|
let spawnError = null;
|
|
25184
25480
|
try {
|
|
@@ -25192,22 +25488,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25192
25488
|
} catch (error) {
|
|
25193
25489
|
spawnError = error;
|
|
25194
25490
|
}
|
|
25195
|
-
|
|
25196
|
-
runtime.recordSpawn({
|
|
25197
|
-
ts: runtime.now(),
|
|
25198
|
-
agentKey: key,
|
|
25199
|
-
root: env.scope,
|
|
25200
|
-
depth: env.depth,
|
|
25201
|
-
childSessionId: sessionId,
|
|
25202
|
-
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25203
|
-
agent,
|
|
25204
|
-
promptPreview: `handoff from ${from}`,
|
|
25205
|
-
outcome: sessionId ? "spawned" : "failed"
|
|
25206
|
-
});
|
|
25207
|
-
} catch (error) {
|
|
25208
|
-
const created = sessionId ? `spawned ${sessionId} but ` : "";
|
|
25209
|
-
return agentFail("spawn_limit_exceeded", `${created}the ledger write failed: ${String(error)}`);
|
|
25210
|
-
}
|
|
25491
|
+
const lineageRecorded = resolveSpawn(runtime, env, key, reservation.spawnId, { agent, promptPreview: preview, sessionId });
|
|
25211
25492
|
if (!sessionId) {
|
|
25212
25493
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
25213
25494
|
}
|
|
@@ -25219,6 +25500,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25219
25500
|
depth: env.depth + 1,
|
|
25220
25501
|
spawned: spawned + 1,
|
|
25221
25502
|
maxSpawn: env.maxSpawn,
|
|
25503
|
+
lineageRecorded,
|
|
25222
25504
|
handoff: { mode: "summary", from }
|
|
25223
25505
|
});
|
|
25224
25506
|
}
|
|
@@ -25493,7 +25775,7 @@ function cliSpawnModelOptions() {
|
|
|
25493
25775
|
}
|
|
25494
25776
|
const DEFAULT_READ_LIMIT = 20;
|
|
25495
25777
|
function scopeOf(runtime) {
|
|
25496
|
-
const env = readAgentEnv(runtime.env);
|
|
25778
|
+
const env = readAgentEnv(runtime.env, runtime.machineBudget);
|
|
25497
25779
|
if (!env) return agentFail("not_agent_env", "This command only runs inside a Saycode-spawned session.");
|
|
25498
25780
|
const machineId = readMachineId(
|
|
25499
25781
|
{ homeDir: runtime.homeDir, env: runtime.env, credentialHome: runtime.credentialHome ?? null },
|
|
@@ -25504,9 +25786,6 @@ function scopeOf(runtime) {
|
|
|
25504
25786
|
}
|
|
25505
25787
|
return { scope: { root: env.scope, machineId }, env };
|
|
25506
25788
|
}
|
|
25507
|
-
function isFailure(value2) {
|
|
25508
|
-
return typeof value2 === "object" && value2 !== null && "stdout" in value2;
|
|
25509
|
-
}
|
|
25510
25789
|
async function resolveTarget(runtime, scope, sessionId) {
|
|
25511
25790
|
const sessions = await runtime.listSessions();
|
|
25512
25791
|
const session = sessions.find((candidate) => candidate.id === sessionId);
|
|
@@ -25557,16 +25836,12 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
|
|
|
25557
25836
|
if (!context.ok) {
|
|
25558
25837
|
return context.reason === "out_of_scope" ? agentFail("out_of_scope", `${context.path} is outside ${env.scope}`) : agentFail("file_unreadable", `${context.path} could not be read`);
|
|
25559
25838
|
}
|
|
25560
|
-
const
|
|
25561
|
-
|
|
25562
|
-
|
|
25563
|
-
spawned = runtime.countSpawns(key);
|
|
25564
|
-
} catch (error) {
|
|
25565
|
-
return agentFail("spawn_limit_exceeded", `spawn ledger is unreadable, refusing: ${String(error)}`);
|
|
25566
|
-
}
|
|
25567
|
-
const decision = spawnDecision({ depth: env.depth, maxSpawn: env.maxSpawn, spawned });
|
|
25568
|
-
if (decision !== "allow") return agentFail(decision, `${spawned}/${env.maxSpawn} used`);
|
|
25839
|
+
const budget = checkSpawnBudget(runtime, env);
|
|
25840
|
+
if (isAgentFailure(budget)) return budget;
|
|
25841
|
+
const { spawned, key } = budget;
|
|
25569
25842
|
const childId = runtime.newAgentId();
|
|
25843
|
+
const reservation = reserveSpawn(runtime, env, key, { agent, promptPreview: text2 }, spawned);
|
|
25844
|
+
if (isAgentFailure(reservation)) return reservation;
|
|
25570
25845
|
let sessionId = null;
|
|
25571
25846
|
let spawnError = null;
|
|
25572
25847
|
try {
|
|
@@ -25580,36 +25855,34 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
|
|
|
25580
25855
|
} catch (error) {
|
|
25581
25856
|
spawnError = error;
|
|
25582
25857
|
}
|
|
25583
|
-
|
|
25584
|
-
runtime.recordSpawn({
|
|
25585
|
-
ts: runtime.now(),
|
|
25586
|
-
agentKey: key,
|
|
25587
|
-
root: env.scope,
|
|
25588
|
-
depth: env.depth,
|
|
25589
|
-
childSessionId: sessionId,
|
|
25590
|
-
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25591
|
-
agent,
|
|
25592
|
-
promptPreview: text2,
|
|
25593
|
-
outcome: sessionId ? "spawned" : "failed"
|
|
25594
|
-
});
|
|
25595
|
-
} catch (error) {
|
|
25596
|
-
const created = sessionId ? `spawned ${sessionId} but ` : "";
|
|
25597
|
-
return agentFail("spawn_limit_exceeded", `${created}the ledger write failed: ${String(error)}`);
|
|
25598
|
-
}
|
|
25858
|
+
const lineageRecorded = resolveSpawn(runtime, env, key, reservation.spawnId, { agent, promptPreview: text2, sessionId });
|
|
25599
25859
|
if (!sessionId) {
|
|
25600
25860
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
25601
25861
|
}
|
|
25602
25862
|
await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
|
|
25603
|
-
return agentOk({
|
|
25863
|
+
return agentOk({
|
|
25864
|
+
sessionId,
|
|
25865
|
+
agent,
|
|
25866
|
+
directory: env.root,
|
|
25867
|
+
depth: env.depth + 1,
|
|
25868
|
+
spawned: spawned + 1,
|
|
25869
|
+
maxSpawn: env.maxSpawn,
|
|
25870
|
+
// False means the child runs but this session's ownership link went
|
|
25871
|
+
// unwritten, so a spawned parent cannot steer or stop it.
|
|
25872
|
+
lineageRecorded
|
|
25873
|
+
});
|
|
25604
25874
|
}
|
|
25605
25875
|
async function runAgentCommand(runtime, args) {
|
|
25606
25876
|
const context = scopeOf(runtime);
|
|
25607
|
-
if (
|
|
25877
|
+
if (isAgentFailure(context)) return context;
|
|
25608
25878
|
const { scope, env } = context;
|
|
25609
25879
|
if (args.verb === "whoami") {
|
|
25610
25880
|
let spawned = null;
|
|
25881
|
+
let resetsAt = null;
|
|
25611
25882
|
try {
|
|
25612
|
-
|
|
25883
|
+
const usage = runtime.countSpawns(spawnBudgetKey(env), env.maxSpawn);
|
|
25884
|
+
spawned = usage.spawned;
|
|
25885
|
+
resetsAt = usage.resetsAt;
|
|
25613
25886
|
} catch {
|
|
25614
25887
|
spawned = null;
|
|
25615
25888
|
}
|
|
@@ -25624,6 +25897,9 @@ async function runAgentCommand(runtime, args) {
|
|
|
25624
25897
|
maxSpawn: env.maxSpawn,
|
|
25625
25898
|
spawned,
|
|
25626
25899
|
spawnsRemaining: spawned === null ? null : Math.max(0, env.maxSpawn - spawned),
|
|
25900
|
+
// The budget is a rolling window, so "full" is a temporary state with a
|
|
25901
|
+
// known end — an agent that is told only "0 left" would give up for good.
|
|
25902
|
+
budgetResetsAt: resetsAt,
|
|
25627
25903
|
canSpawn: env.depth < AGENT_MAX_DEPTH && spawned !== null && spawned < env.maxSpawn,
|
|
25628
25904
|
spawnModelOptions: cliSpawnModelOptions()
|
|
25629
25905
|
});
|
|
@@ -25635,7 +25911,7 @@ async function runAgentCommand(runtime, args) {
|
|
|
25635
25911
|
if (args.verb === "spawn") {
|
|
25636
25912
|
const agent = args.agent ?? DEFAULT_SPAWN_AGENT;
|
|
25637
25913
|
const modelOptions = args.model !== void 0 || args.effort !== void 0 ? await validateSpawnModelOptions(agent, args.model, args.effort) : {};
|
|
25638
|
-
if (
|
|
25914
|
+
if (isAgentFailure(modelOptions)) return modelOptions;
|
|
25639
25915
|
if (args.handoff) {
|
|
25640
25916
|
return handoffToSibling(runtime, scope, env, args.handoff, agent, modelOptions);
|
|
25641
25917
|
}
|
|
@@ -25646,7 +25922,7 @@ async function runAgentCommand(runtime, args) {
|
|
|
25646
25922
|
}
|
|
25647
25923
|
if (!args.sessionId) return agentFail("session_not_found", "no session id given");
|
|
25648
25924
|
const target = await resolveTarget(runtime, scope, args.sessionId);
|
|
25649
|
-
if (
|
|
25925
|
+
if (isAgentFailure(target)) return target;
|
|
25650
25926
|
switch (args.verb) {
|
|
25651
25927
|
case "read":
|
|
25652
25928
|
return readSession(runtime, target, args.last ?? DEFAULT_READ_LIMIT, args.since);
|
|
@@ -25681,7 +25957,7 @@ async function validateSpawnModelOptions(agent, model, effort) {
|
|
|
25681
25957
|
function parseSpawnRecord(value2) {
|
|
25682
25958
|
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
|
|
25683
25959
|
const record2 = value2;
|
|
25684
|
-
return typeof record2.ts === "number" && Number.isFinite(record2.ts) && typeof record2.agentKey === "string" && record2.agentKey.length > 0 && typeof record2.root === "string" && record2.root.length > 0 && typeof record2.depth === "number" && Number.isInteger(record2.depth) && record2.depth >= 0 && (record2.childSessionId === null || typeof record2.childSessionId === "string" && record2.childSessionId.length > 0) && (record2.parentSessionId === void 0 || typeof record2.parentSessionId === "string" && record2.parentSessionId.length > 0) && typeof record2.agent === "string" && record2.agent.length > 0 && typeof record2.promptPreview === "string" && (record2.outcome === "spawned" || record2.outcome === "failed") ? record2 : null;
|
|
25960
|
+
return typeof record2.ts === "number" && Number.isFinite(record2.ts) && typeof record2.agentKey === "string" && record2.agentKey.length > 0 && typeof record2.root === "string" && record2.root.length > 0 && typeof record2.depth === "number" && Number.isInteger(record2.depth) && record2.depth >= 0 && (record2.childSessionId === null || typeof record2.childSessionId === "string" && record2.childSessionId.length > 0) && (record2.parentSessionId === void 0 || typeof record2.parentSessionId === "string" && record2.parentSessionId.length > 0) && (record2.spawnId === void 0 || typeof record2.spawnId === "string" && record2.spawnId.length > 0) && typeof record2.agent === "string" && record2.agent.length > 0 && typeof record2.promptPreview === "string" && (record2.outcome === "spawned" || record2.outcome === "failed") ? record2 : null;
|
|
25685
25961
|
}
|
|
25686
25962
|
const LEDGER_FILE_MODE = 384;
|
|
25687
25963
|
const PREVIEW_MAX_LENGTH = 200;
|
|
@@ -25722,7 +25998,8 @@ function recordIdentity(record2) {
|
|
|
25722
25998
|
record2.parentSessionId ?? null,
|
|
25723
25999
|
record2.agent,
|
|
25724
26000
|
record2.promptPreview,
|
|
25725
|
-
record2.outcome
|
|
26001
|
+
record2.outcome,
|
|
26002
|
+
record2.spawnId ?? null
|
|
25726
26003
|
]);
|
|
25727
26004
|
}
|
|
25728
26005
|
function readSpawnRecords(homeDir, rootDir) {
|
|
@@ -25738,14 +26015,35 @@ function readSpawnRecords(homeDir, rootDir) {
|
|
|
25738
26015
|
return true;
|
|
25739
26016
|
});
|
|
25740
26017
|
}
|
|
25741
|
-
function
|
|
25742
|
-
|
|
26018
|
+
function spawnBudgetUsage(homeDir, rootDir, agentKey, now, maxSpawn) {
|
|
26019
|
+
const mine = readSpawnRecords(homeDir, rootDir).filter((record2) => record2.agentKey === agentKey);
|
|
26020
|
+
return {
|
|
26021
|
+
spawned: chargedSpawnCount(mine, now),
|
|
26022
|
+
resetsAt: spawnBudgetResetsAt(mine, now, maxSpawn)
|
|
26023
|
+
};
|
|
26024
|
+
}
|
|
26025
|
+
function endsWithNewline(path) {
|
|
26026
|
+
let fd;
|
|
26027
|
+
try {
|
|
26028
|
+
fd = openSync(path, "r");
|
|
26029
|
+
const size = fstatSync(fd).size;
|
|
26030
|
+
if (size === 0) return true;
|
|
26031
|
+
const tail = Buffer.alloc(1);
|
|
26032
|
+
readSync(fd, tail, 0, 1, size - 1);
|
|
26033
|
+
return tail[0] === 10;
|
|
26034
|
+
} catch (error) {
|
|
26035
|
+
if (error.code === "ENOENT") return true;
|
|
26036
|
+
throw error;
|
|
26037
|
+
} finally {
|
|
26038
|
+
if (fd !== void 0) closeSync(fd);
|
|
26039
|
+
}
|
|
25743
26040
|
}
|
|
25744
26041
|
function appendSpawnRecord(rootDir, record2) {
|
|
25745
26042
|
const path = agentLineagePath(rootDir);
|
|
25746
26043
|
mkdirSync(join(rootDir, ".aplus"), { recursive: true, mode: 448 });
|
|
25747
26044
|
const entry = { ...record2, promptPreview: record2.promptPreview.slice(0, PREVIEW_MAX_LENGTH) };
|
|
25748
|
-
|
|
26045
|
+
const separator = endsWithNewline(path) ? "" : "\n";
|
|
26046
|
+
appendFileSync(path, `${separator}${JSON.stringify(entry)}
|
|
25749
26047
|
`, { mode: LEDGER_FILE_MODE });
|
|
25750
26048
|
}
|
|
25751
26049
|
function normalize(raw) {
|
|
@@ -25947,13 +26245,16 @@ async function openStream(context, transport, sessionId) {
|
|
|
25947
26245
|
}
|
|
25948
26246
|
function createAgentRuntime(context, transport, env = process.env) {
|
|
25949
26247
|
const keys = [decodeBase64(context.syncCredential.secret)];
|
|
25950
|
-
const
|
|
26248
|
+
const machineBudget = machineSpawnBudget({ cpuCount: cpus().length, totalMemoryBytes: totalmem() });
|
|
26249
|
+
const homeDir = context.homeDir, lineageRoot = readAgentEnv(env, machineBudget)?.scope ?? context.homeDir;
|
|
25951
26250
|
const credentialHome = context.credential.source === "saycode" ? null : dirname(context.credential.path);
|
|
26251
|
+
const now = () => Date.now();
|
|
25952
26252
|
return {
|
|
25953
26253
|
env,
|
|
25954
26254
|
homeDir,
|
|
25955
26255
|
credentialHome,
|
|
25956
|
-
|
|
26256
|
+
machineBudget,
|
|
26257
|
+
now,
|
|
25957
26258
|
readFile: readFileOrNull,
|
|
25958
26259
|
isReadableFile: isReadableFileSync,
|
|
25959
26260
|
listSessions: async () => normalizeSessionRows(await context.rest.listSessions(), keys),
|
|
@@ -25989,7 +26290,7 @@ function createAgentRuntime(context, transport, env = process.env) {
|
|
|
25989
26290
|
return { sessionId: result.sessionId };
|
|
25990
26291
|
},
|
|
25991
26292
|
newAgentId: () => randomBytes(8).toString("hex"),
|
|
25992
|
-
countSpawns: (agentKey) =>
|
|
26293
|
+
countSpawns: (agentKey, maxSpawn) => spawnBudgetUsage(homeDir, lineageRoot, agentKey, now(), maxSpawn),
|
|
25993
26294
|
recordSpawn: (record2) => appendSpawnRecord(lineageRoot, record2),
|
|
25994
26295
|
readLineage: () => readSpawnRecords(homeDir, lineageRoot),
|
|
25995
26296
|
steerSession: (sessionId, text2) => steerAgentSession(transport, sessionId, text2),
|