@buzzni/saycode-cli 0.4.0 → 0.5.0
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 +465 -168
- 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.0";
|
|
44
44
|
const manifest = {
|
|
45
45
|
version: version$1
|
|
46
46
|
};
|
|
@@ -271,6 +271,14 @@ 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
|
+
};
|
|
274
282
|
const taskNotificationKo = {
|
|
275
283
|
"taskNotification.ariaLabel": "백그라운드 작업 알림",
|
|
276
284
|
"taskNotification.status.completed": "작업 완료",
|
|
@@ -1005,7 +1013,11 @@ const memoryEventsKo = {
|
|
|
1005
1013
|
"settings.memoryEvents.unavailable": "기억 대시보드가 실행 중이 아닙니다.",
|
|
1006
1014
|
"settings.memoryEvents.searchLabel": "기억 검색",
|
|
1007
1015
|
"settings.memoryEvents.prevPage": "이전",
|
|
1008
|
-
"settings.memoryEvents.nextPage": "다음"
|
|
1016
|
+
"settings.memoryEvents.nextPage": "다음",
|
|
1017
|
+
"settings.memoryEvents.delete": "삭제",
|
|
1018
|
+
"settings.memoryEvents.confirmDelete": "정말 삭제",
|
|
1019
|
+
"settings.memoryEvents.deleteHint": "삭제한 기억은 되돌릴 수 없습니다.",
|
|
1020
|
+
"settings.memoryEvents.deleteFailed": "삭제하지 못했습니다: {reason}"
|
|
1009
1021
|
};
|
|
1010
1022
|
const externalWorktreeInboxKo = {
|
|
1011
1023
|
"externalWorktreeInbox.title": "앱 밖에서 만든 worktree",
|
|
@@ -1218,7 +1230,46 @@ const accountSettingsKo = {
|
|
|
1218
1230
|
"settingsGroup.devices": "기기",
|
|
1219
1231
|
"settingsGroup.app": "앱",
|
|
1220
1232
|
"shortcuts.action.settings": "환경설정 열기",
|
|
1221
|
-
"shortcuts.action.settingsDescription": "환경설정 창을 엽니다."
|
|
1233
|
+
"shortcuts.action.settingsDescription": "환경설정 창을 엽니다.",
|
|
1234
|
+
"accountKeyRecovery.title": "암호화된 데이터가 잠겨 있습니다",
|
|
1235
|
+
"accountKeyRecovery.description": "이 데스크톱에서 암호화된 데이터를 잠금 해제하려면 E2EE 복구 키를 입력하세요. 복구 키는 이 기기를 벗어나지 않습니다.",
|
|
1236
|
+
"accountKeyRecovery.label": "E2EE 복구 키",
|
|
1237
|
+
"accountKeyRecovery.unavailable": "이 계정에서는 복구 키 잠금 해제를 사용할 수 없습니다.",
|
|
1238
|
+
"accountKeyRecovery.invalid": "복구 키로 암호화된 데이터를 잠금 해제하지 못했습니다.",
|
|
1239
|
+
"accountKeyRecovery.failed": "복구 키 잠금 해제에 실패했습니다.",
|
|
1240
|
+
"accountKeyRecovery.action": "암호화된 데이터 잠금 해제",
|
|
1241
|
+
"accountKeyRecovery.busy": "잠금 해제 중…",
|
|
1242
|
+
"accountKeyDevice.title": "신뢰 기기",
|
|
1243
|
+
"accountKeyDevice.description": "기존 신뢰 기기에서 이 데스크톱을 승인하거나 대기 중인 기기 요청을 검토하세요.",
|
|
1244
|
+
"accountKeyDevice.loading": "신뢰 기기 상태를 불러오는 중…",
|
|
1245
|
+
"accountKeyDevice.loadFailed": "기기 승인 상태를 불러오지 못했습니다.",
|
|
1246
|
+
"accountKeyDevice.stableIdentityUnavailable": "안정적인 기기 ID를 사용할 수 없어 신뢰 기기 등록을 진행할 수 없습니다.",
|
|
1247
|
+
"accountKeyDevice.secureStorageUnavailable": "보안 기기 저장소를 사용할 수 없어 신뢰 기기 등록을 진행할 수 없습니다.",
|
|
1248
|
+
"accountKeyDevice.qrFailed": "기기 승인 QR을 만들지 못했습니다.",
|
|
1249
|
+
"accountKeyDevice.qrAlt": "기기 승인 QR 코드",
|
|
1250
|
+
"accountKeyDevice.requestFailed": "기기 승인 요청에 실패했습니다.",
|
|
1251
|
+
"accountKeyDevice.pending": "신뢰 기기의 승인을 기다리고 있습니다.",
|
|
1252
|
+
"accountKeyDevice.completeFailed": "기기 승인을 완료하지 못했습니다.",
|
|
1253
|
+
"accountKeyDevice.approved": "이 데스크톱이 신뢰 기기로 승인되었습니다.",
|
|
1254
|
+
"accountKeyDevice.cancelled": "이 기기에서 승인 요청을 취소했습니다.",
|
|
1255
|
+
"accountKeyDevice.cancelFailed": "기기 승인 요청을 취소하지 못했습니다.",
|
|
1256
|
+
"accountKeyDevice.requestsFailed": "대기 중인 기기 요청을 불러오지 못했습니다.",
|
|
1257
|
+
"accountKeyDevice.approveFailed": "기기를 승인하지 못했습니다.",
|
|
1258
|
+
"accountKeyDevice.rejectFailed": "기기 요청을 거절하지 못했습니다.",
|
|
1259
|
+
"accountKeyDevice.revokeFailed": "신뢰 기기를 폐기하지 못했습니다.",
|
|
1260
|
+
"accountKeyDevice.cleanupFailed": "기기는 폐기됐지만 로컬 자격 증명을 삭제하지 못했습니다.",
|
|
1261
|
+
"accountKeyDevice.lockPersistFailed": "기기는 폐기됐지만 로컬 세션 잠금을 저장하지 못했습니다.",
|
|
1262
|
+
"accountKeyDevice.request": "기기 승인 요청",
|
|
1263
|
+
"accountKeyDevice.check": "승인 확인",
|
|
1264
|
+
"accountKeyDevice.cancel": "요청 취소",
|
|
1265
|
+
"accountKeyDevice.scanBefore": "{time} 전에 승인된 기기에서 이 QR을 스캔하세요.",
|
|
1266
|
+
"accountKeyDevice.expires": "{time}에 만료",
|
|
1267
|
+
"accountKeyDevice.approve": "기기 승인",
|
|
1268
|
+
"accountKeyDevice.reject": "기기 거절",
|
|
1269
|
+
"accountKeyDevice.refresh": "요청 새로 고침",
|
|
1270
|
+
"accountKeyDevice.current": "현재 데스크톱",
|
|
1271
|
+
"accountKeyDevice.lastUsed": "마지막 사용: {time}",
|
|
1272
|
+
"accountKeyDevice.revoke": "신뢰 기기 폐기"
|
|
1222
1273
|
};
|
|
1223
1274
|
const projectScopeKo = {
|
|
1224
1275
|
"projectScope.protected": "프로젝트 범위 보호됨",
|
|
@@ -1556,6 +1607,7 @@ const composerQuickCommandsKo = {
|
|
|
1556
1607
|
const aiCommitPrKo = {
|
|
1557
1608
|
"aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
|
|
1558
1609
|
"aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
|
|
1610
|
+
"aiCommitPr.stagedUnknown": "스테이지된 변경을 확인하지 못했습니다.",
|
|
1559
1611
|
"aiCommitPr.stageAll": "모든 변경 스테이지",
|
|
1560
1612
|
"aiCommitPr.staging": "스테이지하는 중…",
|
|
1561
1613
|
"aiCommitPr.messagePlaceholder": "커밋 메시지",
|
|
@@ -1633,6 +1685,14 @@ const connectorReadinessKo = {
|
|
|
1633
1685
|
"connectorReadiness.state.unknown": "상태 미확인"
|
|
1634
1686
|
};
|
|
1635
1687
|
const checkpointKo = {
|
|
1688
|
+
"checkpoint.activation.label": "파일 체크포인트 보호",
|
|
1689
|
+
"checkpoint.activation.title": "에이전트가 파일을 변경하기 전에 세션별 체크포인트를 만듭니다.",
|
|
1690
|
+
"checkpoint.activation.hint": "세션별",
|
|
1691
|
+
"checkpoint.activation.unavailable.platform": "macOS에서만 지원",
|
|
1692
|
+
"checkpoint.activation.unavailable.agent": "Claude와 Codex만 지원",
|
|
1693
|
+
"checkpoint.activation.unavailable.sandbox": "프로젝트 샌드박스 필요",
|
|
1694
|
+
"checkpoint.activation.unavailable.worktree": "작업 사본은 준비 중",
|
|
1695
|
+
"checkpoint.activation.error": "파일 체크포인트 보호를 사용할 수 없습니다: {reason}",
|
|
1636
1696
|
"checkpoint.title": "파일 체크포인트",
|
|
1637
1697
|
"checkpoint.status.protected": "보호됨",
|
|
1638
1698
|
"checkpoint.status.legacy": "보호되지 않음",
|
|
@@ -1793,7 +1853,7 @@ const settingsInfoStructureKo = {
|
|
|
1793
1853
|
"overflowMenu.aria": "추가 액션 메뉴",
|
|
1794
1854
|
"overflowMenu.title": "추가 액션"
|
|
1795
1855
|
};
|
|
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 };
|
|
1856
|
+
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
1857
|
const onboardingKo = {
|
|
1798
1858
|
"boot.stage.app": "앱 준비 중…",
|
|
1799
1859
|
"boot.stage.localRuntime": "로컬 작업공간 준비 중…",
|
|
@@ -2605,6 +2665,7 @@ const ko = {
|
|
|
2605
2665
|
"gitControls.branchLoading": "Git branch 확인 중",
|
|
2606
2666
|
"gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
|
|
2607
2667
|
"gitControls.checkFailed": "Git 확인 실패",
|
|
2668
|
+
"gitControls.retryStatus": "다시 확인",
|
|
2608
2669
|
"gitControls.checking": "Git 확인 중",
|
|
2609
2670
|
"gitControls.manageBranchAria": "{branch} branch 관리",
|
|
2610
2671
|
"gitControls.manageBranchTitle": "Branch 변경 및 생성",
|
|
@@ -3653,6 +3714,9 @@ const ko = {
|
|
|
3653
3714
|
"preview.tokenUrlInvalid": "Preview 토큰 URL이 올바르지 않습니다.",
|
|
3654
3715
|
"branchDialog.loading": "Branch 목록을 불러오는 중입니다.",
|
|
3655
3716
|
"branchDialog.empty": "표시할 branch가 없습니다.",
|
|
3717
|
+
"branchDialog.searchLabel": "브랜치 검색",
|
|
3718
|
+
"branchDialog.searchPlaceholder": "브랜치 이름 검색",
|
|
3719
|
+
"branchDialog.searchEmpty": "'{query}'와 일치하는 브랜치가 없습니다.",
|
|
3656
3720
|
"branchDialog.actionsAria": "{name} 브랜치 작업",
|
|
3657
3721
|
"branchDialog.rowAria": "{name} {meta}",
|
|
3658
3722
|
"branchDialog.switch": "브랜치 이동",
|
|
@@ -3667,6 +3731,24 @@ const ko = {
|
|
|
3667
3731
|
"branchDialog.deleteConfirm": "'{name}' 브랜치를 삭제할까요? 머지되지 않은 커밋이 있으면 Git이 거부합니다.",
|
|
3668
3732
|
"branchDialog.deleteConfirmAction": "삭제",
|
|
3669
3733
|
"branchDialog.cancel": "취소",
|
|
3734
|
+
"branchCleanup.openAria": "병합된 브랜치 정리",
|
|
3735
|
+
"branchCleanup.title": "정리 가능한 브랜치",
|
|
3736
|
+
"branchCleanup.loading": "브랜치를 확인하는 중…",
|
|
3737
|
+
"branchCleanup.empty": "정리 가능한 브랜치가 없습니다.",
|
|
3738
|
+
"branchCleanup.scannedNote": "로컬 브랜치 {total}개 중 {count}개가 정리 가능합니다.",
|
|
3739
|
+
"branchCleanup.selectAll": "모두 선택",
|
|
3740
|
+
"branchCleanup.deselectAll": "모두 해제",
|
|
3741
|
+
"branchCleanup.deleteSelected": "{count}개 삭제",
|
|
3742
|
+
"branchCleanup.deleteFailed": "브랜치를 삭제할 수 없습니다.",
|
|
3743
|
+
"branchCleanup.deletePartial": "{total}개 중 {failed}개를 삭제하지 못했습니다: {detail}",
|
|
3744
|
+
"branchCleanup.loadFailed": "브랜치 확인을 실패했습니다.",
|
|
3745
|
+
"branchCleanup.noBaseBranch": "기본 브랜치를 찾을 수 없습니다. origin/HEAD 를 설정하거나 프로젝트 설정에서 기본 브랜치를 지정하세요.",
|
|
3746
|
+
"branchCleanup.probeIncomplete": "브랜치 정보를 완전히 읽지 못했습니다. 목록이 비어 보일 수 있으니 다시 시도하세요.",
|
|
3747
|
+
"branchCleanup.demoUnsupported": "Demo 모드에서는 지원하지 않습니다.",
|
|
3748
|
+
"branchCleanup.refreshAria": "브랜치 목록 새로고침",
|
|
3749
|
+
"branchCleanup.squashScanning": "squash merge된 브랜치를 찾는 중…",
|
|
3750
|
+
"branchCleanup.squashFailed": "squash merge 검사에 실패했습니다. 아래 목록은 일반 merge 후보만입니다.",
|
|
3751
|
+
"branchCleanup.squashTag": "squash 병합",
|
|
3670
3752
|
"message.deleteConfirm": "이 메시지를 삭제할까요? 이 작업은 새로고침 후에도 유지됩니다.",
|
|
3671
3753
|
"message.deleteFailed": "메시지를 삭제할 수 없습니다.",
|
|
3672
3754
|
"message.sendFailed": "메시지를 보낼 수 없습니다.",
|
|
@@ -7531,6 +7613,7 @@ function parseUnlockMethod(value2) {
|
|
|
7531
7613
|
const kdfParams = parseKdfParams(method.kdfParams);
|
|
7532
7614
|
const salt = boundedAllowEmpty(method.salt, 256);
|
|
7533
7615
|
const wrappedMasterKey = bounded(method.wrappedMasterKey, 16384);
|
|
7616
|
+
const possessionPublicKey = bounded(method.possessionPublicKey, 43);
|
|
7534
7617
|
if (!id || !/^[A-Za-z0-9_-]+$/.test(id) || type !== "password" && type !== "recovery" && type !== "device" || !kdfAlgorithm || !kdfParams || salt === void 0 || !wrappedMasterKey) return void 0;
|
|
7535
7618
|
const revision = method.revision;
|
|
7536
7619
|
if (revision !== void 0 && (!Number.isSafeInteger(revision) || revision < 1)) return void 0;
|
|
@@ -7541,6 +7624,7 @@ function parseUnlockMethod(value2) {
|
|
|
7541
7624
|
kdfParams,
|
|
7542
7625
|
salt,
|
|
7543
7626
|
wrappedMasterKey,
|
|
7627
|
+
...possessionPublicKey && /^[A-Za-z0-9_-]{43}$/.test(possessionPublicKey) ? { possessionPublicKey } : {},
|
|
7544
7628
|
...revision === void 0 ? {} : { revision }
|
|
7545
7629
|
};
|
|
7546
7630
|
}
|
|
@@ -7674,6 +7758,19 @@ async function decryptDataKeyToString(dataEncryptionKey, bundleBase64) {
|
|
|
7674
7758
|
return null;
|
|
7675
7759
|
}
|
|
7676
7760
|
}
|
|
7761
|
+
async function decryptDataKeyJson(dataEncryptionKey, bundleBase64) {
|
|
7762
|
+
const json = await decryptDataKeyToString(dataEncryptionKey, bundleBase64);
|
|
7763
|
+
if (json === null) return null;
|
|
7764
|
+
try {
|
|
7765
|
+
return JSON.parse(json);
|
|
7766
|
+
} catch {
|
|
7767
|
+
return null;
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
async function decryptDataKeyRecord(dataEncryptionKey, bundleBase64) {
|
|
7771
|
+
const value2 = await decryptDataKeyJson(dataEncryptionKey, bundleBase64);
|
|
7772
|
+
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
7773
|
+
}
|
|
7677
7774
|
async function encryptDataKeyContent(dataEncryptionKey, value2) {
|
|
7678
7775
|
const nonce = nacl.randomBytes(GCM_NONCE_LENGTH);
|
|
7679
7776
|
const key = await importGcmKey(dataEncryptionKey);
|
|
@@ -7687,6 +7784,9 @@ async function encryptDataKeyContent(dataEncryptionKey, value2) {
|
|
|
7687
7784
|
}
|
|
7688
7785
|
let candidates = [];
|
|
7689
7786
|
let version = 0;
|
|
7787
|
+
function dataKeyUnwrapCandidates(accountPrivateKey, companyKeys = getCompanyKeyCandidates()) {
|
|
7788
|
+
return accountPrivateKey ? [accountPrivateKey, ...companyKeys] : [];
|
|
7789
|
+
}
|
|
7690
7790
|
function getCompanyKeyCandidates() {
|
|
7691
7791
|
return candidates.map((candidate) => candidate.privateKey);
|
|
7692
7792
|
}
|
|
@@ -7696,9 +7796,25 @@ function companyKeyChainVersion() {
|
|
|
7696
7796
|
const dekBySessionId = /* @__PURE__ */ new Map();
|
|
7697
7797
|
const lockedSessionIds = /* @__PURE__ */ new Set();
|
|
7698
7798
|
const fingerprints = /* @__PURE__ */ new Map();
|
|
7699
|
-
|
|
7799
|
+
const companyUnwrappedEnvelopes = /* @__PURE__ */ new Map();
|
|
7800
|
+
function firstUnwrap(envelope, candidates2) {
|
|
7801
|
+
for (const candidate of candidates2) {
|
|
7802
|
+
const dek = unwrapDataEncryptionKey(envelope, candidate);
|
|
7803
|
+
if (dek) return dek;
|
|
7804
|
+
}
|
|
7805
|
+
return null;
|
|
7806
|
+
}
|
|
7807
|
+
let boundaryGeneration$1 = 0;
|
|
7700
7808
|
function sessionDekRegistryGeneration() {
|
|
7701
|
-
return boundaryGeneration;
|
|
7809
|
+
return boundaryGeneration$1;
|
|
7810
|
+
}
|
|
7811
|
+
function adoptSessionDek(sessionId, envelope, dek, byCompanyKey) {
|
|
7812
|
+
const existing = dekBySessionId.get(sessionId);
|
|
7813
|
+
if (existing && existing.length === dek.length && existing.every((byte, index) => byte === dek[index])) dek.fill(0);
|
|
7814
|
+
else dekBySessionId.set(sessionId, dek);
|
|
7815
|
+
lockedSessionIds.delete(sessionId);
|
|
7816
|
+
if (byCompanyKey) companyUnwrappedEnvelopes.set(sessionId, envelope);
|
|
7817
|
+
else companyUnwrappedEnvelopes.delete(sessionId);
|
|
7702
7818
|
}
|
|
7703
7819
|
function registerSessionDek(session, accountPrivateKey) {
|
|
7704
7820
|
const envelope = session.dataEncryptionKey ?? null;
|
|
@@ -7706,6 +7822,7 @@ function registerSessionDek(session, accountPrivateKey) {
|
|
|
7706
7822
|
dekBySessionId.delete(session.id);
|
|
7707
7823
|
lockedSessionIds.delete(session.id);
|
|
7708
7824
|
fingerprints.delete(session.id);
|
|
7825
|
+
companyUnwrappedEnvelopes.delete(session.id);
|
|
7709
7826
|
return;
|
|
7710
7827
|
}
|
|
7711
7828
|
const fingerprint = `${envelope}|${accountPrivateKey ? "1" : "0"}|${companyKeyChainVersion()}`;
|
|
@@ -7713,20 +7830,16 @@ function registerSessionDek(session, accountPrivateKey) {
|
|
|
7713
7830
|
if (!accountPrivateKey && dekBySessionId.has(session.id) && fingerprints.get(session.id)?.startsWith(`${envelope}|1|`)) return;
|
|
7714
7831
|
const previousFingerprint = fingerprints.get(session.id);
|
|
7715
7832
|
fingerprints.set(session.id, fingerprint);
|
|
7716
|
-
const
|
|
7717
|
-
|
|
7718
|
-
for (const candidate of candidates2) {
|
|
7719
|
-
dek = unwrapDataEncryptionKey(envelope, candidate);
|
|
7720
|
-
if (dek) break;
|
|
7721
|
-
}
|
|
7833
|
+
const accountDek = accountPrivateKey ? unwrapDataEncryptionKey(envelope, accountPrivateKey) : null;
|
|
7834
|
+
const dek = accountDek ?? (accountPrivateKey ? firstUnwrap(envelope, getCompanyKeyCandidates()) : null);
|
|
7722
7835
|
if (dek) {
|
|
7723
|
-
|
|
7724
|
-
lockedSessionIds.delete(session.id);
|
|
7836
|
+
adoptSessionDek(session.id, envelope, dek, accountDek === null);
|
|
7725
7837
|
return;
|
|
7726
7838
|
}
|
|
7727
7839
|
if (dekBySessionId.has(session.id) && previousFingerprint?.startsWith(`${envelope}|`)) return;
|
|
7728
7840
|
dekBySessionId.delete(session.id);
|
|
7729
7841
|
lockedSessionIds.add(session.id);
|
|
7842
|
+
if (companyUnwrappedEnvelopes.has(session.id)) companyUnwrappedEnvelopes.set(session.id, envelope);
|
|
7730
7843
|
}
|
|
7731
7844
|
function getSessionDek(sessionId) {
|
|
7732
7845
|
return dekBySessionId.get(sessionId) ?? null;
|
|
@@ -7768,6 +7881,60 @@ async function preDecryptSessionHeaders(sessions, accountPrivateKey, sinceGenera
|
|
|
7768
7881
|
}
|
|
7769
7882
|
}));
|
|
7770
7883
|
}
|
|
7884
|
+
const machineDekById = /* @__PURE__ */ new Map();
|
|
7885
|
+
let boundaryGeneration = 0;
|
|
7886
|
+
function machineDekRegistryGeneration() {
|
|
7887
|
+
return boundaryGeneration;
|
|
7888
|
+
}
|
|
7889
|
+
function discardMachineDek(machineId) {
|
|
7890
|
+
machineDekById.get(machineId)?.dek.fill(0);
|
|
7891
|
+
machineDekById.delete(machineId);
|
|
7892
|
+
}
|
|
7893
|
+
function machineFingerprint(envelope) {
|
|
7894
|
+
return `${envelope}|${companyKeyChainVersion()}`;
|
|
7895
|
+
}
|
|
7896
|
+
async function resolveMachineDataKeyFields(record2, legacyKeys, privateKeys, scope) {
|
|
7897
|
+
const discard = { machineId: record2.id, fields: null, entry: null };
|
|
7898
|
+
if (!record2.dataEncryptionKey || !record2.metadata) return discard;
|
|
7899
|
+
if (decryptJsonWithCandidates(record2.metadata, legacyKeys)) return discard;
|
|
7900
|
+
const fingerprint = machineFingerprint(record2.dataEncryptionKey);
|
|
7901
|
+
const cached = machineDekById.get(record2.id);
|
|
7902
|
+
const machineKey = cached?.fingerprint === fingerprint ? cached.dek : unwrapMachineKey(record2.dataEncryptionKey, privateKeys);
|
|
7903
|
+
if (!machineKey) return discard;
|
|
7904
|
+
const metadata = await decryptDataKeyRecord(machineKey, record2.metadata);
|
|
7905
|
+
if (!metadata) return discard;
|
|
7906
|
+
const daemonState = record2.daemonState ? await decryptDataKeyRecord(machineKey, record2.daemonState) : null;
|
|
7907
|
+
const entry = cached?.dek === machineKey ? cached : { dek: machineKey, scope, fingerprint };
|
|
7908
|
+
entry.scope = scope;
|
|
7909
|
+
return { machineId: record2.id, fields: { metadata, daemonState }, entry };
|
|
7910
|
+
}
|
|
7911
|
+
function commitMachineDek(resolved) {
|
|
7912
|
+
if (resolved.entry) machineDekById.set(resolved.machineId, resolved.entry);
|
|
7913
|
+
else discardMachineDek(resolved.machineId);
|
|
7914
|
+
}
|
|
7915
|
+
function unwrapMachineKey(envelope, privateKeys) {
|
|
7916
|
+
for (const privateKey of privateKeys) {
|
|
7917
|
+
const machineKey = unwrapDataEncryptionKey(envelope, privateKey);
|
|
7918
|
+
if (machineKey) return machineKey;
|
|
7919
|
+
}
|
|
7920
|
+
return null;
|
|
7921
|
+
}
|
|
7922
|
+
async function decryptMachineDataKeyRows(rows, legacyKeys, options, scope, sinceGeneration = machineDekRegistryGeneration()) {
|
|
7923
|
+
const none = () => rows.map(() => null);
|
|
7924
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7925
|
+
const listed = new Set(rows.map((row2) => row2.id));
|
|
7926
|
+
for (const [machineId, entry] of [...machineDekById]) {
|
|
7927
|
+
if (entry.scope === scope && !listed.has(machineId)) discardMachineDek(machineId);
|
|
7928
|
+
}
|
|
7929
|
+
if (!options.accountPrivateKey || !rows.some((row2) => row2.dataEncryptionKey)) return none();
|
|
7930
|
+
await options.dataKeyReady;
|
|
7931
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7932
|
+
const privateKeys = dataKeyUnwrapCandidates(options.accountPrivateKey);
|
|
7933
|
+
const resolved = await Promise.all(rows.map((row2) => resolveMachineDataKeyFields(row2, legacyKeys, privateKeys, scope)));
|
|
7934
|
+
if (sinceGeneration !== boundaryGeneration) return none();
|
|
7935
|
+
for (const entry of resolved) commitMachineDek(entry);
|
|
7936
|
+
return resolved.map((entry) => entry.fields);
|
|
7937
|
+
}
|
|
7771
7938
|
function buildUserTextMessageContent(input) {
|
|
7772
7939
|
return {
|
|
7773
7940
|
role: "user",
|
|
@@ -17951,10 +18118,10 @@ function normalizeAutomationSupport(value2) {
|
|
|
17951
18118
|
...Number.isSafeInteger(capability.keyVersion) && capability.keyVersion > 0 ? { keyVersion: capability.keyVersion } : {}
|
|
17952
18119
|
};
|
|
17953
18120
|
}
|
|
17954
|
-
function normalizeMachine(row2, candidateKeys) {
|
|
18121
|
+
function normalizeMachine(row2, candidateKeys, dataKeyFields = null) {
|
|
17955
18122
|
const registry = row2.registry;
|
|
17956
|
-
const decrypted = decryptJsonWithCandidates(row2.metadata, candidateKeys) ?? {};
|
|
17957
|
-
const daemonState = decryptJsonWithCandidates(row2.daemonState, candidateKeys);
|
|
18123
|
+
const decrypted = dataKeyFields?.metadata ?? decryptJsonWithCandidates(row2.metadata, candidateKeys) ?? {};
|
|
18124
|
+
const daemonState = dataKeyFields?.daemonState ?? decryptJsonWithCandidates(row2.daemonState, candidateKeys);
|
|
17958
18125
|
const resources = row2.resources ?? parseMachineResources(daemonState);
|
|
17959
18126
|
const host = row2.host || registry?.host || (typeof decrypted.host === "string" ? decrypted.host : row2.id.slice(0, 8));
|
|
17960
18127
|
const platform2 = row2.platform || registry?.platform || (typeof decrypted.platform === "string" ? decrypted.platform : "unknown");
|
|
@@ -19180,13 +19347,16 @@ class ApiClient {
|
|
|
19180
19347
|
async getMachines(companyId, candidateKeys = [], options = {}) {
|
|
19181
19348
|
const company = companyId ?? "personal";
|
|
19182
19349
|
const statusParam = options.withStatus ? "&withStatus=1" : "";
|
|
19350
|
+
const dekGeneration = machineDekRegistryGeneration();
|
|
19183
19351
|
const data = await this.request(
|
|
19184
19352
|
`/api/machines/list?company=${encodeURIComponent(company)}${statusParam}`,
|
|
19185
19353
|
{ boundary: "api" }
|
|
19186
19354
|
);
|
|
19187
19355
|
const rows = Array.isArray(data) ? data : data.machines ?? [];
|
|
19188
19356
|
const registry = await this.fetchMachineRegistry(rows.map((row2) => row2.id));
|
|
19189
|
-
|
|
19357
|
+
const enriched = rows.map((row2) => withMachineRegistry(row2, registry));
|
|
19358
|
+
const dataKeyFields = await decryptMachineDataKeyRows(enriched, candidateKeys, options, company, dekGeneration);
|
|
19359
|
+
return enriched.map((row2, index) => normalizeMachine(row2, candidateKeys, dataKeyFields[index]));
|
|
19190
19360
|
}
|
|
19191
19361
|
/**
|
|
19192
19362
|
* Fail-open: a registry outage degrades machine names back to the daemon
|
|
@@ -21813,6 +21983,141 @@ function resumeHistoryCount(input) {
|
|
|
21813
21983
|
if (!input.resuming) return 0;
|
|
21814
21984
|
return input.since ?? DEFAULT_RESUME_HISTORY;
|
|
21815
21985
|
}
|
|
21986
|
+
const AGENT_SPAWN_BUDGET_MIN = 4;
|
|
21987
|
+
const AGENT_SPAWN_BUDGET_MAX = 32;
|
|
21988
|
+
const MEMORY_BYTES_PER_AGENT = 2 * 1024 ** 3;
|
|
21989
|
+
const AGENTS_PER_CORE = 2;
|
|
21990
|
+
function machineSpawnBudget(capacity) {
|
|
21991
|
+
const cores = Number.isFinite(capacity.cpuCount) ? Math.max(0, Math.floor(capacity.cpuCount)) : 0;
|
|
21992
|
+
const memory = Number.isFinite(capacity.totalMemoryBytes) ? Math.max(0, capacity.totalMemoryBytes) : 0;
|
|
21993
|
+
if (cores <= 0 || memory <= 0) return AGENT_SPAWN_BUDGET_MIN;
|
|
21994
|
+
const budget = Math.min(Math.floor(memory / MEMORY_BYTES_PER_AGENT), cores * AGENTS_PER_CORE);
|
|
21995
|
+
return Math.min(Math.max(budget, AGENT_SPAWN_BUDGET_MIN), AGENT_SPAWN_BUDGET_MAX);
|
|
21996
|
+
}
|
|
21997
|
+
function tierSpawnBudget(depth, machineBudget) {
|
|
21998
|
+
return depth <= 0 ? machineBudget : Math.ceil(machineBudget / 2);
|
|
21999
|
+
}
|
|
22000
|
+
function resolveMaxSpawn(input) {
|
|
22001
|
+
const tier = tierSpawnBudget(input.depth, input.machineBudget);
|
|
22002
|
+
return input.policy === null ? tier : Math.min(input.policy, tier);
|
|
22003
|
+
}
|
|
22004
|
+
const SPAWN_BUDGET_WINDOW_MS = 6 * 60 * 60 * 1e3;
|
|
22005
|
+
function isCharged(chargedAt, now) {
|
|
22006
|
+
return chargedAt > now - SPAWN_BUDGET_WINDOW_MS;
|
|
22007
|
+
}
|
|
22008
|
+
function chargeTimes(records) {
|
|
22009
|
+
const byId = /* @__PURE__ */ new Map();
|
|
22010
|
+
const unkeyed = [];
|
|
22011
|
+
for (const record2 of records) {
|
|
22012
|
+
if (record2.spawnId === void 0) {
|
|
22013
|
+
unkeyed.push(record2.ts);
|
|
22014
|
+
continue;
|
|
22015
|
+
}
|
|
22016
|
+
const seen = byId.get(record2.spawnId);
|
|
22017
|
+
if (seen === void 0 || record2.ts < seen) byId.set(record2.spawnId, record2.ts);
|
|
22018
|
+
}
|
|
22019
|
+
return [...byId.values(), ...unkeyed];
|
|
22020
|
+
}
|
|
22021
|
+
function chargedSpawnCount(records, now) {
|
|
22022
|
+
return chargeTimes(records).filter((chargedAt) => isCharged(chargedAt, now)).length;
|
|
22023
|
+
}
|
|
22024
|
+
function spawnBudgetResetsAt(records, now, maxSpawn) {
|
|
22025
|
+
if (maxSpawn <= 0) return null;
|
|
22026
|
+
const expiries = chargeTimes(records).filter((chargedAt) => isCharged(chargedAt, now)).map((ts) => ts + SPAWN_BUDGET_WINDOW_MS).sort((a, b) => a - b);
|
|
22027
|
+
if (expiries.length < maxSpawn) return null;
|
|
22028
|
+
return expiries[expiries.length - maxSpawn];
|
|
22029
|
+
}
|
|
22030
|
+
function spawnLimitDetail(input) {
|
|
22031
|
+
const used = `${input.spawned}/${input.maxSpawn} used`;
|
|
22032
|
+
if (input.resetsAt === null || input.resetsAt <= input.now) return used;
|
|
22033
|
+
const minutes = Math.ceil((input.resetsAt - input.now) / 6e4);
|
|
22034
|
+
return `${used}; the next slot frees in ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
22035
|
+
}
|
|
22036
|
+
const AGENT_ENV_FLAG = "SAYCODE_AGENT_ENV";
|
|
22037
|
+
const AGENT_MAX_DEPTH = 2;
|
|
22038
|
+
function readOptionalNonNegativeInt(raw) {
|
|
22039
|
+
if (raw === void 0 || !/^\d+$/.test(raw.trim())) return null;
|
|
22040
|
+
const value2 = Number(raw.trim());
|
|
22041
|
+
return Number.isSafeInteger(value2) ? value2 : null;
|
|
22042
|
+
}
|
|
22043
|
+
function normalizeDirectory(raw) {
|
|
22044
|
+
const trimmed = raw.trim();
|
|
22045
|
+
if (!trimmed) return "";
|
|
22046
|
+
const stripped = trimmed.replace(/\/+$/, "");
|
|
22047
|
+
return stripped || "/";
|
|
22048
|
+
}
|
|
22049
|
+
function hasAgentMarker(env) {
|
|
22050
|
+
return env[AGENT_ENV_FLAG] === "1";
|
|
22051
|
+
}
|
|
22052
|
+
function isAgentEnv(env) {
|
|
22053
|
+
return hasAgentMarker(env) && normalizeDirectory(env.SAYCODE_AGENT_ROOT ?? "") !== "";
|
|
22054
|
+
}
|
|
22055
|
+
function readAgentEnv(env, machineBudget) {
|
|
22056
|
+
if (!isAgentEnv(env)) return null;
|
|
22057
|
+
const root = normalizeDirectory(env.SAYCODE_AGENT_ROOT ?? "");
|
|
22058
|
+
const depth = readOptionalNonNegativeInt(env.SAYCODE_AGENT_DEPTH) ?? 0;
|
|
22059
|
+
return {
|
|
22060
|
+
root,
|
|
22061
|
+
scope: normalizeDirectory(env.SAYCODE_AGENT_SCOPE ?? "") || root,
|
|
22062
|
+
depth,
|
|
22063
|
+
maxSpawn: resolveMaxSpawn({
|
|
22064
|
+
policy: readOptionalNonNegativeInt(env.SAYCODE_AGENT_MAX_SPAWN),
|
|
22065
|
+
depth,
|
|
22066
|
+
machineBudget
|
|
22067
|
+
}),
|
|
22068
|
+
agentId: env.SAYCODE_AGENT_ID?.trim() || null,
|
|
22069
|
+
sessionId: env.APLUS_SESSION_ID?.trim() || null
|
|
22070
|
+
};
|
|
22071
|
+
}
|
|
22072
|
+
function spawnDecision(input) {
|
|
22073
|
+
if (input.depth >= AGENT_MAX_DEPTH) return "spawn_depth_exceeded";
|
|
22074
|
+
if (input.spawned >= input.maxSpawn) return "spawn_limit_exceeded";
|
|
22075
|
+
return "allow";
|
|
22076
|
+
}
|
|
22077
|
+
function childAgentEnv(parent, childAgentId) {
|
|
22078
|
+
return {
|
|
22079
|
+
[AGENT_ENV_FLAG]: "1",
|
|
22080
|
+
SAYCODE_AGENT_ROOT: parent.root,
|
|
22081
|
+
SAYCODE_AGENT_SCOPE: parent.scope,
|
|
22082
|
+
SAYCODE_AGENT_DEPTH: String(parent.depth + 1),
|
|
22083
|
+
SAYCODE_AGENT_MAX_SPAWN: String(tierSpawnBudget(parent.depth + 1, parent.maxSpawn)),
|
|
22084
|
+
SAYCODE_AGENT_ID: childAgentId
|
|
22085
|
+
};
|
|
22086
|
+
}
|
|
22087
|
+
function isInScope(session, scope) {
|
|
22088
|
+
if (!session.machineId || !scope.machineId) return false;
|
|
22089
|
+
if (session.machineId !== scope.machineId) return false;
|
|
22090
|
+
const root = normalizeDirectory(scope.root);
|
|
22091
|
+
const directory = normalizeDirectory(session.directory);
|
|
22092
|
+
if (!root || !directory) return false;
|
|
22093
|
+
if (directory === root) return true;
|
|
22094
|
+
return directory.startsWith(root === "/" ? "/" : `${root}/`);
|
|
22095
|
+
}
|
|
22096
|
+
function sessionsInScope(sessions, scope) {
|
|
22097
|
+
return sessions.filter((session) => isInScope(session, scope));
|
|
22098
|
+
}
|
|
22099
|
+
function machineSettingsPaths(input) {
|
|
22100
|
+
const override = input.env.SAYCODE_HAPPY_HOME?.trim();
|
|
22101
|
+
const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
|
|
22102
|
+
const homes = CREDENTIAL_SOURCE_ORDER.filter((source) => source !== "saycode").map((source) => `${input.homeDir}/.${source}`);
|
|
22103
|
+
const ordered = [input.credentialHome?.trim(), override, runtimeHome, ...homes];
|
|
22104
|
+
return [...new Set(ordered.filter((home) => Boolean(home)))].map((home) => `${home}/settings.json`.replace(/\/{2,}/g, "/"));
|
|
22105
|
+
}
|
|
22106
|
+
function readMachineId(input, readFile2) {
|
|
22107
|
+
for (const path of machineSettingsPaths(input)) {
|
|
22108
|
+
const raw = readFile2(path);
|
|
22109
|
+
if (!raw) continue;
|
|
22110
|
+
try {
|
|
22111
|
+
const parsed = JSON.parse(raw);
|
|
22112
|
+
if (typeof parsed?.machineId === "string" && parsed.machineId) return parsed.machineId;
|
|
22113
|
+
} catch {
|
|
22114
|
+
}
|
|
22115
|
+
}
|
|
22116
|
+
return null;
|
|
22117
|
+
}
|
|
22118
|
+
function spawnBudgetKey(env) {
|
|
22119
|
+
return env.agentId ?? env.root;
|
|
22120
|
+
}
|
|
21816
22121
|
const REAL_CLI_SESSION_CONNECTION_DEPS = {
|
|
21817
22122
|
createLocalId: () => `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`
|
|
21818
22123
|
};
|
|
@@ -23891,6 +24196,9 @@ async function askTemporaryLine(question) {
|
|
|
23891
24196
|
}
|
|
23892
24197
|
async function runChat(context, options, providedTransport) {
|
|
23893
24198
|
const resuming = !!options.sessionId;
|
|
24199
|
+
if (!resuming && hasAgentMarker(process.env)) {
|
|
24200
|
+
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.");
|
|
24201
|
+
}
|
|
23894
24202
|
let optionPrompter = null;
|
|
23895
24203
|
let optionInput = null;
|
|
23896
24204
|
if (!resuming && options.promptForOptions) {
|
|
@@ -24795,6 +25103,9 @@ function waitForSessionState(options) {
|
|
|
24795
25103
|
});
|
|
24796
25104
|
}
|
|
24797
25105
|
const AGENT_FAILURE_EXIT_CODE = 3;
|
|
25106
|
+
function isAgentFailure(value2) {
|
|
25107
|
+
return typeof value2 === "object" && value2 !== null && "stdout" in value2;
|
|
25108
|
+
}
|
|
24798
25109
|
function line(value2) {
|
|
24799
25110
|
return `${JSON.stringify(value2)}
|
|
24800
25111
|
`;
|
|
@@ -24927,87 +25238,6 @@ async function promptTarget(runtime, target, text2, options) {
|
|
|
24927
25238
|
stream2.close();
|
|
24928
25239
|
}
|
|
24929
25240
|
}
|
|
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
25241
|
function normalizeAbsolutePath(raw) {
|
|
25012
25242
|
const trimmed = raw.trim();
|
|
25013
25243
|
if (!trimmed.startsWith("/")) return "";
|
|
@@ -25118,6 +25348,71 @@ function buildAgentHandoffPrompt(source, context) {
|
|
|
25118
25348
|
...FIRST_ACTIONS.map((action) => `- ${action}`)
|
|
25119
25349
|
].join("\n");
|
|
25120
25350
|
}
|
|
25351
|
+
function checkSpawnBudget(runtime, env) {
|
|
25352
|
+
const key = spawnBudgetKey(env);
|
|
25353
|
+
let usage;
|
|
25354
|
+
try {
|
|
25355
|
+
usage = runtime.countSpawns(key, env.maxSpawn);
|
|
25356
|
+
} catch (error) {
|
|
25357
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger is unreadable, refusing: ${String(error)}`);
|
|
25358
|
+
}
|
|
25359
|
+
const decision = spawnDecision({ depth: env.depth, maxSpawn: env.maxSpawn, spawned: usage.spawned });
|
|
25360
|
+
if (decision === "allow") return { spawned: usage.spawned, key };
|
|
25361
|
+
return agentFail(decision, spawnLimitDetail({
|
|
25362
|
+
spawned: usage.spawned,
|
|
25363
|
+
maxSpawn: env.maxSpawn,
|
|
25364
|
+
resetsAt: usage.resetsAt,
|
|
25365
|
+
now: runtime.now()
|
|
25366
|
+
}));
|
|
25367
|
+
}
|
|
25368
|
+
function reserveSpawn(runtime, env, key, attempt2, spawnedBefore) {
|
|
25369
|
+
const spawnId = runtime.newAgentId();
|
|
25370
|
+
try {
|
|
25371
|
+
runtime.recordSpawn({
|
|
25372
|
+
ts: runtime.now(),
|
|
25373
|
+
agentKey: key,
|
|
25374
|
+
root: env.scope,
|
|
25375
|
+
depth: env.depth,
|
|
25376
|
+
// Unknown until the daemon answers; `resolveSpawn` appends the id under
|
|
25377
|
+
// the same `spawnId`, and the counter charges the pair once.
|
|
25378
|
+
childSessionId: null,
|
|
25379
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25380
|
+
agent: attempt2.agent,
|
|
25381
|
+
promptPreview: attempt2.promptPreview,
|
|
25382
|
+
outcome: "failed",
|
|
25383
|
+
spawnId
|
|
25384
|
+
});
|
|
25385
|
+
} catch (error) {
|
|
25386
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger is unwritable, refusing before anything is created: ${String(error)}`);
|
|
25387
|
+
}
|
|
25388
|
+
try {
|
|
25389
|
+
if (runtime.countSpawns(key, env.maxSpawn).spawned <= spawnedBefore) {
|
|
25390
|
+
return agentFail("spawn_limit_exceeded", "the spawn reservation did not land in the ledger, refusing before anything is created");
|
|
25391
|
+
}
|
|
25392
|
+
} catch (error) {
|
|
25393
|
+
return agentFail("spawn_limit_exceeded", `spawn ledger became unreadable after reserving, refusing: ${String(error)}`);
|
|
25394
|
+
}
|
|
25395
|
+
return { spawnId };
|
|
25396
|
+
}
|
|
25397
|
+
function resolveSpawn(runtime, env, key, spawnId, outcome) {
|
|
25398
|
+
try {
|
|
25399
|
+
runtime.recordSpawn({
|
|
25400
|
+
ts: runtime.now(),
|
|
25401
|
+
agentKey: key,
|
|
25402
|
+
root: env.scope,
|
|
25403
|
+
depth: env.depth,
|
|
25404
|
+
childSessionId: outcome.sessionId,
|
|
25405
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
25406
|
+
agent: outcome.agent,
|
|
25407
|
+
promptPreview: outcome.promptPreview,
|
|
25408
|
+
outcome: outcome.sessionId ? "spawned" : "failed",
|
|
25409
|
+
spawnId
|
|
25410
|
+
});
|
|
25411
|
+
return true;
|
|
25412
|
+
} catch {
|
|
25413
|
+
return false;
|
|
25414
|
+
}
|
|
25415
|
+
}
|
|
25121
25416
|
async function fileSpawnedSessionUnderProject(runtime, sessionId, directory) {
|
|
25122
25417
|
if (!runtime.registerSpawnedSession) return;
|
|
25123
25418
|
try {
|
|
@@ -25163,15 +25458,9 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25163
25458
|
if (env.depth >= AGENT_MAX_DEPTH) {
|
|
25164
25459
|
return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
|
|
25165
25460
|
}
|
|
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`);
|
|
25461
|
+
const budget = checkSpawnBudget(runtime, env);
|
|
25462
|
+
if (isAgentFailure(budget)) return budget;
|
|
25463
|
+
const { spawned, key } = budget;
|
|
25175
25464
|
let prompt;
|
|
25176
25465
|
try {
|
|
25177
25466
|
prompt = buildHandoffPrompt(await runtime.readMessages(from, HANDOFF_READ_MESSAGES), source, agent);
|
|
@@ -25179,6 +25468,9 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25179
25468
|
return agentFail("handoff_context_failed", String(error));
|
|
25180
25469
|
}
|
|
25181
25470
|
const childId = runtime.newAgentId();
|
|
25471
|
+
const preview = `handoff from ${from}`;
|
|
25472
|
+
const reservation = reserveSpawn(runtime, env, key, { agent, promptPreview: preview }, spawned);
|
|
25473
|
+
if (isAgentFailure(reservation)) return reservation;
|
|
25182
25474
|
let sessionId = null;
|
|
25183
25475
|
let spawnError = null;
|
|
25184
25476
|
try {
|
|
@@ -25192,22 +25484,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25192
25484
|
} catch (error) {
|
|
25193
25485
|
spawnError = error;
|
|
25194
25486
|
}
|
|
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
|
-
}
|
|
25487
|
+
const lineageRecorded = resolveSpawn(runtime, env, key, reservation.spawnId, { agent, promptPreview: preview, sessionId });
|
|
25211
25488
|
if (!sessionId) {
|
|
25212
25489
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
25213
25490
|
}
|
|
@@ -25219,6 +25496,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOption
|
|
|
25219
25496
|
depth: env.depth + 1,
|
|
25220
25497
|
spawned: spawned + 1,
|
|
25221
25498
|
maxSpawn: env.maxSpawn,
|
|
25499
|
+
lineageRecorded,
|
|
25222
25500
|
handoff: { mode: "summary", from }
|
|
25223
25501
|
});
|
|
25224
25502
|
}
|
|
@@ -25493,7 +25771,7 @@ function cliSpawnModelOptions() {
|
|
|
25493
25771
|
}
|
|
25494
25772
|
const DEFAULT_READ_LIMIT = 20;
|
|
25495
25773
|
function scopeOf(runtime) {
|
|
25496
|
-
const env = readAgentEnv(runtime.env);
|
|
25774
|
+
const env = readAgentEnv(runtime.env, runtime.machineBudget);
|
|
25497
25775
|
if (!env) return agentFail("not_agent_env", "This command only runs inside a Saycode-spawned session.");
|
|
25498
25776
|
const machineId = readMachineId(
|
|
25499
25777
|
{ homeDir: runtime.homeDir, env: runtime.env, credentialHome: runtime.credentialHome ?? null },
|
|
@@ -25504,9 +25782,6 @@ function scopeOf(runtime) {
|
|
|
25504
25782
|
}
|
|
25505
25783
|
return { scope: { root: env.scope, machineId }, env };
|
|
25506
25784
|
}
|
|
25507
|
-
function isFailure(value2) {
|
|
25508
|
-
return typeof value2 === "object" && value2 !== null && "stdout" in value2;
|
|
25509
|
-
}
|
|
25510
25785
|
async function resolveTarget(runtime, scope, sessionId) {
|
|
25511
25786
|
const sessions = await runtime.listSessions();
|
|
25512
25787
|
const session = sessions.find((candidate) => candidate.id === sessionId);
|
|
@@ -25557,16 +25832,12 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
|
|
|
25557
25832
|
if (!context.ok) {
|
|
25558
25833
|
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
25834
|
}
|
|
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`);
|
|
25835
|
+
const budget = checkSpawnBudget(runtime, env);
|
|
25836
|
+
if (isAgentFailure(budget)) return budget;
|
|
25837
|
+
const { spawned, key } = budget;
|
|
25569
25838
|
const childId = runtime.newAgentId();
|
|
25839
|
+
const reservation = reserveSpawn(runtime, env, key, { agent, promptPreview: text2 }, spawned);
|
|
25840
|
+
if (isAgentFailure(reservation)) return reservation;
|
|
25570
25841
|
let sessionId = null;
|
|
25571
25842
|
let spawnError = null;
|
|
25572
25843
|
try {
|
|
@@ -25580,36 +25851,34 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], model
|
|
|
25580
25851
|
} catch (error) {
|
|
25581
25852
|
spawnError = error;
|
|
25582
25853
|
}
|
|
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
|
-
}
|
|
25854
|
+
const lineageRecorded = resolveSpawn(runtime, env, key, reservation.spawnId, { agent, promptPreview: text2, sessionId });
|
|
25599
25855
|
if (!sessionId) {
|
|
25600
25856
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
25601
25857
|
}
|
|
25602
25858
|
await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
|
|
25603
|
-
return agentOk({
|
|
25859
|
+
return agentOk({
|
|
25860
|
+
sessionId,
|
|
25861
|
+
agent,
|
|
25862
|
+
directory: env.root,
|
|
25863
|
+
depth: env.depth + 1,
|
|
25864
|
+
spawned: spawned + 1,
|
|
25865
|
+
maxSpawn: env.maxSpawn,
|
|
25866
|
+
// False means the child runs but this session's ownership link went
|
|
25867
|
+
// unwritten, so a spawned parent cannot steer or stop it.
|
|
25868
|
+
lineageRecorded
|
|
25869
|
+
});
|
|
25604
25870
|
}
|
|
25605
25871
|
async function runAgentCommand(runtime, args) {
|
|
25606
25872
|
const context = scopeOf(runtime);
|
|
25607
|
-
if (
|
|
25873
|
+
if (isAgentFailure(context)) return context;
|
|
25608
25874
|
const { scope, env } = context;
|
|
25609
25875
|
if (args.verb === "whoami") {
|
|
25610
25876
|
let spawned = null;
|
|
25877
|
+
let resetsAt = null;
|
|
25611
25878
|
try {
|
|
25612
|
-
|
|
25879
|
+
const usage = runtime.countSpawns(spawnBudgetKey(env), env.maxSpawn);
|
|
25880
|
+
spawned = usage.spawned;
|
|
25881
|
+
resetsAt = usage.resetsAt;
|
|
25613
25882
|
} catch {
|
|
25614
25883
|
spawned = null;
|
|
25615
25884
|
}
|
|
@@ -25624,6 +25893,9 @@ async function runAgentCommand(runtime, args) {
|
|
|
25624
25893
|
maxSpawn: env.maxSpawn,
|
|
25625
25894
|
spawned,
|
|
25626
25895
|
spawnsRemaining: spawned === null ? null : Math.max(0, env.maxSpawn - spawned),
|
|
25896
|
+
// The budget is a rolling window, so "full" is a temporary state with a
|
|
25897
|
+
// known end — an agent that is told only "0 left" would give up for good.
|
|
25898
|
+
budgetResetsAt: resetsAt,
|
|
25627
25899
|
canSpawn: env.depth < AGENT_MAX_DEPTH && spawned !== null && spawned < env.maxSpawn,
|
|
25628
25900
|
spawnModelOptions: cliSpawnModelOptions()
|
|
25629
25901
|
});
|
|
@@ -25635,7 +25907,7 @@ async function runAgentCommand(runtime, args) {
|
|
|
25635
25907
|
if (args.verb === "spawn") {
|
|
25636
25908
|
const agent = args.agent ?? DEFAULT_SPAWN_AGENT;
|
|
25637
25909
|
const modelOptions = args.model !== void 0 || args.effort !== void 0 ? await validateSpawnModelOptions(agent, args.model, args.effort) : {};
|
|
25638
|
-
if (
|
|
25910
|
+
if (isAgentFailure(modelOptions)) return modelOptions;
|
|
25639
25911
|
if (args.handoff) {
|
|
25640
25912
|
return handoffToSibling(runtime, scope, env, args.handoff, agent, modelOptions);
|
|
25641
25913
|
}
|
|
@@ -25646,7 +25918,7 @@ async function runAgentCommand(runtime, args) {
|
|
|
25646
25918
|
}
|
|
25647
25919
|
if (!args.sessionId) return agentFail("session_not_found", "no session id given");
|
|
25648
25920
|
const target = await resolveTarget(runtime, scope, args.sessionId);
|
|
25649
|
-
if (
|
|
25921
|
+
if (isAgentFailure(target)) return target;
|
|
25650
25922
|
switch (args.verb) {
|
|
25651
25923
|
case "read":
|
|
25652
25924
|
return readSession(runtime, target, args.last ?? DEFAULT_READ_LIMIT, args.since);
|
|
@@ -25681,7 +25953,7 @@ async function validateSpawnModelOptions(agent, model, effort) {
|
|
|
25681
25953
|
function parseSpawnRecord(value2) {
|
|
25682
25954
|
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
|
|
25683
25955
|
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;
|
|
25956
|
+
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
25957
|
}
|
|
25686
25958
|
const LEDGER_FILE_MODE = 384;
|
|
25687
25959
|
const PREVIEW_MAX_LENGTH = 200;
|
|
@@ -25722,7 +25994,8 @@ function recordIdentity(record2) {
|
|
|
25722
25994
|
record2.parentSessionId ?? null,
|
|
25723
25995
|
record2.agent,
|
|
25724
25996
|
record2.promptPreview,
|
|
25725
|
-
record2.outcome
|
|
25997
|
+
record2.outcome,
|
|
25998
|
+
record2.spawnId ?? null
|
|
25726
25999
|
]);
|
|
25727
26000
|
}
|
|
25728
26001
|
function readSpawnRecords(homeDir, rootDir) {
|
|
@@ -25738,14 +26011,35 @@ function readSpawnRecords(homeDir, rootDir) {
|
|
|
25738
26011
|
return true;
|
|
25739
26012
|
});
|
|
25740
26013
|
}
|
|
25741
|
-
function
|
|
25742
|
-
|
|
26014
|
+
function spawnBudgetUsage(homeDir, rootDir, agentKey, now, maxSpawn) {
|
|
26015
|
+
const mine = readSpawnRecords(homeDir, rootDir).filter((record2) => record2.agentKey === agentKey);
|
|
26016
|
+
return {
|
|
26017
|
+
spawned: chargedSpawnCount(mine, now),
|
|
26018
|
+
resetsAt: spawnBudgetResetsAt(mine, now, maxSpawn)
|
|
26019
|
+
};
|
|
26020
|
+
}
|
|
26021
|
+
function endsWithNewline(path) {
|
|
26022
|
+
let fd;
|
|
26023
|
+
try {
|
|
26024
|
+
fd = openSync(path, "r");
|
|
26025
|
+
const size = fstatSync(fd).size;
|
|
26026
|
+
if (size === 0) return true;
|
|
26027
|
+
const tail = Buffer.alloc(1);
|
|
26028
|
+
readSync(fd, tail, 0, 1, size - 1);
|
|
26029
|
+
return tail[0] === 10;
|
|
26030
|
+
} catch (error) {
|
|
26031
|
+
if (error.code === "ENOENT") return true;
|
|
26032
|
+
throw error;
|
|
26033
|
+
} finally {
|
|
26034
|
+
if (fd !== void 0) closeSync(fd);
|
|
26035
|
+
}
|
|
25743
26036
|
}
|
|
25744
26037
|
function appendSpawnRecord(rootDir, record2) {
|
|
25745
26038
|
const path = agentLineagePath(rootDir);
|
|
25746
26039
|
mkdirSync(join(rootDir, ".aplus"), { recursive: true, mode: 448 });
|
|
25747
26040
|
const entry = { ...record2, promptPreview: record2.promptPreview.slice(0, PREVIEW_MAX_LENGTH) };
|
|
25748
|
-
|
|
26041
|
+
const separator = endsWithNewline(path) ? "" : "\n";
|
|
26042
|
+
appendFileSync(path, `${separator}${JSON.stringify(entry)}
|
|
25749
26043
|
`, { mode: LEDGER_FILE_MODE });
|
|
25750
26044
|
}
|
|
25751
26045
|
function normalize(raw) {
|
|
@@ -25947,13 +26241,16 @@ async function openStream(context, transport, sessionId) {
|
|
|
25947
26241
|
}
|
|
25948
26242
|
function createAgentRuntime(context, transport, env = process.env) {
|
|
25949
26243
|
const keys = [decodeBase64(context.syncCredential.secret)];
|
|
25950
|
-
const
|
|
26244
|
+
const machineBudget = machineSpawnBudget({ cpuCount: cpus().length, totalMemoryBytes: totalmem() });
|
|
26245
|
+
const homeDir = context.homeDir, lineageRoot = readAgentEnv(env, machineBudget)?.scope ?? context.homeDir;
|
|
25951
26246
|
const credentialHome = context.credential.source === "saycode" ? null : dirname(context.credential.path);
|
|
26247
|
+
const now = () => Date.now();
|
|
25952
26248
|
return {
|
|
25953
26249
|
env,
|
|
25954
26250
|
homeDir,
|
|
25955
26251
|
credentialHome,
|
|
25956
|
-
|
|
26252
|
+
machineBudget,
|
|
26253
|
+
now,
|
|
25957
26254
|
readFile: readFileOrNull,
|
|
25958
26255
|
isReadableFile: isReadableFileSync,
|
|
25959
26256
|
listSessions: async () => normalizeSessionRows(await context.rest.listSessions(), keys),
|
|
@@ -25989,7 +26286,7 @@ function createAgentRuntime(context, transport, env = process.env) {
|
|
|
25989
26286
|
return { sessionId: result.sessionId };
|
|
25990
26287
|
},
|
|
25991
26288
|
newAgentId: () => randomBytes(8).toString("hex"),
|
|
25992
|
-
countSpawns: (agentKey) =>
|
|
26289
|
+
countSpawns: (agentKey, maxSpawn) => spawnBudgetUsage(homeDir, lineageRoot, agentKey, now(), maxSpawn),
|
|
25993
26290
|
recordSpawn: (record2) => appendSpawnRecord(lineageRoot, record2),
|
|
25994
26291
|
readLineage: () => readSpawnRecords(homeDir, lineageRoot),
|
|
25995
26292
|
steerSession: (sessionId, text2) => steerAgentSession(transport, sessionId, text2),
|