@goodandready/dsh-cron 0.3.20 → 0.3.21
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 +4 -3
- package/lib/api-collection.js +2 -1
- package/lib/client.js +16 -2
- package/lib/cron-tool.js +5 -1
- package/lib/locales.json +1 -0
- package/lib/runner.js +26 -5
- package/lib/scheduler-completion.js +8 -4
- package/lib/store.js +69 -3
- package/lib/task-create.js +2 -1
- package/lib/task-patch.js +10 -0
- package/lib/task-transfer.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,8 +96,9 @@ graph TD
|
|
|
96
96
|
- **Automated Synthesis**: Aggregates worker outputs into a structured Markdown digest or delegates synthesis to a designated synthesizer model.
|
|
97
97
|
- **Swarm Metrics & History**: Run history records total, succeeded, failed counts, and duration alongside detailed per-worker execution logs.
|
|
98
98
|
|
|
99
|
-
### 🔍 Git Diff & Interactive Patch Review (#351)
|
|
100
|
-
- **
|
|
99
|
+
### 🔍 Git Diff & Interactive Patch Review (#351, #379)
|
|
100
|
+
- **Scoped Diff Capture**: Tasks running in Git worktrees capture modified files, stat summary, and unified diffs (up to 256 KiB) automatically. Non-worktree tasks ignore repository diffs by default, avoiding storage bloat, but can opt in explicitly via `captureGitDiff: true`.
|
|
101
|
+
- **Lightweight Archive Storage**: Historical run archives automatically strip heavy raw diff payloads into concise excerpts with byte-budget caps (max 15MB) and automatic cleanup of orphaned `.tmp.*` files.
|
|
101
102
|
- **1-Click Discard**: Revert uncommitted working directory changes safely with a single click (`git reset --hard HEAD && git clean -fd`).
|
|
102
103
|
- **1-Click Apply**: Re-apply previously captured diffs directly to the repository (`git apply --whitespace=fix`).
|
|
103
104
|
- **Interactive Run Preview**: Color-coded diff inspector embedded directly into the execution history drawer.
|
|
@@ -215,7 +216,7 @@ Every task picks its own runtime; non-LLM runtimes need no model and consume no
|
|
|
215
216
|
|
|
216
217
|
### 8. Session Integration & Permissions
|
|
217
218
|
* **Real Assistant Output & Terminal Status Extraction** — Isolates current turn session events, extracts the actual assistant message text (excluding previous turn history in persistent sessions), and validates terminal turn status (`turn/end` errors or interruptions) to guarantee accurate reporting, chaining, and structured action execution.
|
|
218
|
-
* **Per-task permission presets** — `default`, `read-only`, `workspace-write`, or `full` are applied to the task's agent session before the prompt runs.
|
|
219
|
+
* **Per-task permission presets & unattended approval** — `default`, `read-only`, `workspace-write`, or `danger-full-access` (aliased as `full`) are mapped directly to host `SandboxMode` and applied to the task's agent session before the prompt runs. For unattended background runs, approval policy is pinned to `never` via host `approval` service to eliminate interactive approval stalls.
|
|
219
220
|
* **Session auto-archive** — isolated cron sessions are archived after each run (best-effort) so they do not clutter the chat list.
|
|
220
221
|
* **History → session navigation** — every LLM run records its session; open it straight from the run history entry.
|
|
221
222
|
|
package/lib/api-collection.js
CHANGED
|
@@ -169,7 +169,7 @@ export function buildTaskRecord(store, body, taskType, parsed) {
|
|
|
169
169
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
170
170
|
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
171
171
|
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
172
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
172
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
173
173
|
validators: Array.isArray(body.validators) ? sanitizeValidators(body.validators) : (current ? current.validators : undefined),
|
|
174
174
|
...mergeExecutionFields(current, body, parsed),
|
|
175
175
|
};
|
|
@@ -194,6 +194,7 @@ export function mergeExecutionFields(current, body, parsed) {
|
|
|
194
194
|
workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
|
|
195
195
|
worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
|
|
196
196
|
keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
|
|
197
|
+
captureGitDiff: body.captureGitDiff !== undefined ? Boolean(body.captureGitDiff) : (body.captureDiff !== undefined ? Boolean(body.captureDiff) : (current ? current.captureGitDiff : undefined)),
|
|
197
198
|
httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
|
|
198
199
|
httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
|
|
199
200
|
httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
|
package/lib/client.js
CHANGED
|
@@ -1441,6 +1441,7 @@ const [formCwd, setFormCwd] = React.useState('');
|
|
|
1441
1441
|
const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
|
|
1442
1442
|
const [formWorktree, setFormWorktree] = React.useState(false);
|
|
1443
1443
|
const [formKeepWorktree, setFormKeepWorktree] = React.useState(false);
|
|
1444
|
+
const [formCaptureGitDiff, setFormCaptureGitDiff] = React.useState(false);
|
|
1444
1445
|
const [formHttpMethod, setFormHttpMethod] = React.useState('GET');
|
|
1445
1446
|
const [formHttpUrl, setFormHttpUrl] = React.useState('');
|
|
1446
1447
|
const [formHttpHeaders, setFormHttpHeaders] = React.useState('');
|
|
@@ -1496,6 +1497,7 @@ cwd: String(formCwd || '').trim(),
|
|
|
1496
1497
|
workspaceId: String(formWorkspaceId || '').trim(),
|
|
1497
1498
|
worktree: Boolean(formWorktree),
|
|
1498
1499
|
keepWorktree: Boolean(formKeepWorktree),
|
|
1500
|
+
captureGitDiff: Boolean(formCaptureGitDiff),
|
|
1499
1501
|
httpMethod: String(formHttpMethod || 'GET'),
|
|
1500
1502
|
httpUrl: String(formHttpUrl || '').trim(),
|
|
1501
1503
|
httpHttpHeaders: String(formHttpHeaders || '').trim(),
|
|
@@ -2130,6 +2132,7 @@ setFormCwd('');
|
|
|
2130
2132
|
setFormWorkspaceId('');
|
|
2131
2133
|
setFormWorktree(false);
|
|
2132
2134
|
setFormKeepWorktree(false);
|
|
2135
|
+
setFormCaptureGitDiff(false);
|
|
2133
2136
|
setFormHttpMethod('GET');
|
|
2134
2137
|
setFormHttpUrl('');
|
|
2135
2138
|
setFormHttpHeaders('');
|
|
@@ -2217,6 +2220,7 @@ cwd: '',
|
|
|
2217
2220
|
workspaceId: '',
|
|
2218
2221
|
worktree: false,
|
|
2219
2222
|
keepWorktree: false,
|
|
2223
|
+
captureGitDiff: false,
|
|
2220
2224
|
httpMethod: 'GET',
|
|
2221
2225
|
httpUrl: '',
|
|
2222
2226
|
httpHttpHeaders: '',
|
|
@@ -2342,7 +2346,7 @@ setSchedulePreviewRuns(null);
|
|
|
2342
2346
|
setFormTimezone(task.timezone || '');
|
|
2343
2347
|
setFormMisfirePolicy(task.misfirePolicy || 'skip');
|
|
2344
2348
|
setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
|
|
2345
|
-
setFormPermissionPreset(task.permissionPreset || 'default');
|
|
2349
|
+
setFormPermissionPreset(task.permissionPreset === 'full' ? 'danger-full-access' : (task.permissionPreset || 'default'));
|
|
2346
2350
|
setFormAgentPreset(task.agentPreset || '');
|
|
2347
2351
|
setFormSessionTitle(task.sessionTitle || '');
|
|
2348
2352
|
setFormTargetSessionId(task.targetSessionId || '');
|
|
@@ -2352,6 +2356,7 @@ setFormCwd(task.cwd || '');
|
|
|
2352
2356
|
setFormWorkspaceId(task.workspaceId || '');
|
|
2353
2357
|
setFormWorktree(Boolean(task.worktree));
|
|
2354
2358
|
setFormKeepWorktree(Boolean(task.keepWorktree));
|
|
2359
|
+
setFormCaptureGitDiff(task.captureGitDiff !== undefined ? Boolean(task.captureGitDiff) : Boolean(task.worktree));
|
|
2355
2360
|
setFormHttpMethod(task.httpMethod || 'GET');
|
|
2356
2361
|
setFormHttpUrl(task.httpUrl || '');
|
|
2357
2362
|
setFormHttpHeaders(typeof task.httpHeaders === 'string' ? task.httpHeaders : (task.httpHeaders ? JSON.stringify(task.httpHeaders) : ''));
|
|
@@ -2426,6 +2431,7 @@ cwd: formCwd.trim(),
|
|
|
2426
2431
|
workspaceId: formWorkspaceId.trim(),
|
|
2427
2432
|
worktree: isAgentType ? Boolean(formWorktree) : false,
|
|
2428
2433
|
keepWorktree: isAgentType ? Boolean(formKeepWorktree) : false,
|
|
2434
|
+
captureGitDiff: Boolean(formCaptureGitDiff),
|
|
2429
2435
|
httpMethod: formHttpMethod,
|
|
2430
2436
|
httpUrl: formHttpUrl.trim(),
|
|
2431
2437
|
httpHeaders: formHttpHeaders.trim() || undefined,
|
|
@@ -3423,7 +3429,7 @@ onChange: (e) => setFormPermissionPreset(e.target.value)
|
|
|
3423
3429
|
_h('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
|
|
3424
3430
|
_h('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
|
|
3425
3431
|
_h('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
|
|
3426
|
-
_h('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
|
|
3432
|
+
_h('option', { value: 'danger-full-access' }, T_KEY(t, 'form.permissionFull'))
|
|
3427
3433
|
)
|
|
3428
3434
|
)
|
|
3429
3435
|
)
|
|
@@ -3683,6 +3689,14 @@ checked: formKeepWorktree,
|
|
|
3683
3689
|
onChange: (e) => setFormKeepWorktree(e.target.checked)
|
|
3684
3690
|
}),
|
|
3685
3691
|
_h('span', null, T_KEY(t, 'form.keepWorktreeLabel'))
|
|
3692
|
+
),
|
|
3693
|
+
_h('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: formWorktree ? '24px' : '0px', marginTop: '6px' } },
|
|
3694
|
+
_h('input', {
|
|
3695
|
+
type: 'checkbox',
|
|
3696
|
+
checked: formCaptureGitDiff,
|
|
3697
|
+
onChange: (e) => setFormCaptureGitDiff(e.target.checked)
|
|
3698
|
+
}),
|
|
3699
|
+
_h('span', null, T_KEY(t, 'form.captureGitDiffLabel'))
|
|
3686
3700
|
)
|
|
3687
3701
|
),
|
|
3688
3702
|
formType !== 'http' && _h('div', { className: 'dsh-cron-form-group' },
|
package/lib/cron-tool.js
CHANGED
|
@@ -149,9 +149,13 @@ export const cronToolParameters = {
|
|
|
149
149
|
},
|
|
150
150
|
permissionPreset: {
|
|
151
151
|
type: 'string',
|
|
152
|
-
enum: ['default', 'read-only', 'workspace-write', 'full'],
|
|
152
|
+
enum: ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'],
|
|
153
153
|
description: 'Permission preset applied to the task session (only used for agent session tasks: llm, skill, workflow)',
|
|
154
154
|
},
|
|
155
|
+
captureGitDiff: {
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
description: 'Whether to capture git diff on run completion. Defaults to true for worktree tasks and false for non-worktree tasks.',
|
|
158
|
+
},
|
|
155
159
|
sessionTitle: {
|
|
156
160
|
type: 'string',
|
|
157
161
|
description: 'Custom session title template for isolated agent runs (supports {{date}} placeholder, e.g. "{{date}} [Daily] Report")',
|
package/lib/locales.json
CHANGED
|
@@ -179,6 +179,7 @@
|
|
|
179
179
|
"form.permissionDefault": "默认 (继承主配置)",
|
|
180
180
|
"form.permissionRestricted": "受限模式 (只读)",
|
|
181
181
|
"form.permissionFull": "完全访问权限",
|
|
182
|
+
"form.captureGitDiffLabel": "捕获 Git Diff (任务结束时记录代码变更)",
|
|
182
183
|
"form.envLabel": "环境变量 (KEY=VALUE, 每行一个)",
|
|
183
184
|
"form.envPlaceholder": "FOO=bar\\nAPI_KEY=secret",
|
|
184
185
|
"form.cwdLabel": "工作目录 (绝对路径)",
|
package/lib/runner.js
CHANGED
|
@@ -644,16 +644,37 @@ export class SessionRunner {
|
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
/** Per-job permission preset (#32): read-only | workspace-write | full. */
|
|
647
|
+
/** Per-job permission preset (#32, #380, GH #18): read-only | workspace-write | danger-full-access (or full alias). */
|
|
648
648
|
_applyPermissionPreset(task, handle) {
|
|
649
|
-
if (!task.permissionPreset || task.permissionPreset === 'default')
|
|
649
|
+
if (!task.permissionPreset || task.permissionPreset === 'default') {
|
|
650
|
+
this._pinApprovalPolicy(handle);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const targetPreset = task.permissionPreset === 'full' ? 'danger-full-access' : task.permissionPreset;
|
|
650
654
|
try {
|
|
651
|
-
const presets = this.ctx.get
|
|
655
|
+
const presets = this.ctx.get ? this.ctx.get('permissionPresets') : this.ctx.permissionPresets;
|
|
652
656
|
if (presets && typeof presets.set === 'function') {
|
|
653
|
-
presets.set(handle.agent.session,
|
|
657
|
+
presets.set(handle.agent.session, targetPreset);
|
|
654
658
|
}
|
|
655
659
|
} catch (ppErr) {
|
|
656
|
-
logger.warn(
|
|
660
|
+
logger.warn(`[dsh-cron] permission preset "${task.permissionPreset}" (target: "${targetPreset}") not applied to task ${task.id || ''}:`, ppErr.message);
|
|
661
|
+
}
|
|
662
|
+
this._pinApprovalPolicy(handle);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
_pinApprovalPolicy(handle) {
|
|
666
|
+
try {
|
|
667
|
+
const approval = this.ctx.get ? this.ctx.get('approval') : this.ctx.approval;
|
|
668
|
+
if (approval && typeof approval.setPolicy === 'function') {
|
|
669
|
+
if (handle?.agent) {
|
|
670
|
+
bestEffort('approval-set-policy-agent', () => approval.setPolicy(handle.agent, 'never'), logger);
|
|
671
|
+
}
|
|
672
|
+
if (handle?.agent?.session) {
|
|
673
|
+
bestEffort('approval-set-policy-session', () => approval.setPolicy(handle.agent.session, 'never'), logger);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch (appErr) {
|
|
677
|
+
logger.warn('[dsh-cron] approval setPolicy not applied:', appErr.message);
|
|
657
678
|
}
|
|
658
679
|
}
|
|
659
680
|
|
|
@@ -142,16 +142,20 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
142
142
|
runInfo.swarmStats = outcome.swarmStats;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Capture git diff if task runs in a worktree or
|
|
146
|
-
const
|
|
145
|
+
// Capture git diff if task runs in a worktree or explicitly opted in (#351, #379, GH #17)
|
|
146
|
+
const explicitCapture = (typeof task.captureGitDiff === 'boolean')
|
|
147
|
+
? task.captureGitDiff
|
|
148
|
+
: ((typeof task.captureDiff === 'boolean') ? task.captureDiff : null);
|
|
149
|
+
const shouldCaptureDiff = explicitCapture !== null ? explicitCapture : Boolean(task.worktree);
|
|
150
|
+
const diffDir = shouldCaptureDiff ? (task.worktree ? (runInfo.worktreePath || task.cwd) : task.cwd) : null;
|
|
147
151
|
if (diffDir) {
|
|
148
152
|
try {
|
|
149
153
|
const diffResult = await captureGitDiff(diffDir);
|
|
150
154
|
if (diffResult && diffResult.hasChanges) {
|
|
151
155
|
runInfo.gitDiff = diffResult;
|
|
152
156
|
}
|
|
153
|
-
} catch {
|
|
154
|
-
|
|
157
|
+
} catch (diffErr) {
|
|
158
|
+
bestEffort('log-diff-error', () => scheduler?.logger?.debug?.(`[dsh-cron] git diff capture failed: ${diffErr?.message}`));
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
package/lib/store.js
CHANGED
|
@@ -60,6 +60,7 @@ export class TaskStore {
|
|
|
60
60
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
61
|
}
|
|
62
62
|
bestEffort('chmod-dir', () => fs.chmodSync(dir, 0o700));
|
|
63
|
+
this._cleanOrphanedTmpFiles(dir);
|
|
63
64
|
const bakPath = `${this.filePath}.bak`;
|
|
64
65
|
|
|
65
66
|
const loadData = (raw) => {
|
|
@@ -823,6 +824,17 @@ export class TaskStore {
|
|
|
823
824
|
},
|
|
824
825
|
...prevRuns,
|
|
825
826
|
];
|
|
827
|
+
// Truncate raw diff in older active history entries to bound memory and JSON size (#379, GH #17)
|
|
828
|
+
for (let i = 10; i < runs.length; i++) {
|
|
829
|
+
const r = runs[i];
|
|
830
|
+
if (r && r.gitDiff && r.gitDiff.diff && r.gitDiff.diff.length > 2000) {
|
|
831
|
+
r.gitDiff = {
|
|
832
|
+
...r.gitDiff,
|
|
833
|
+
diff: r.gitDiff.diff.slice(0, 2000) + '\n... [diff truncated in history]',
|
|
834
|
+
truncated: true,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
826
838
|
// Keep up to 100 runs in active store; archive older runs to tasks-history-archive.json (#134)
|
|
827
839
|
if (runs.length > 100) {
|
|
828
840
|
const overflow = runs.splice(100);
|
|
@@ -833,7 +845,32 @@ export class TaskStore {
|
|
|
833
845
|
}
|
|
834
846
|
|
|
835
847
|
/**
|
|
836
|
-
*
|
|
848
|
+
* Remove orphaned temporary write files older than 10 minutes (#379, GH #17).
|
|
849
|
+
*/
|
|
850
|
+
_cleanOrphanedTmpFiles(dir) {
|
|
851
|
+
try {
|
|
852
|
+
if (!fs.existsSync(dir)) return;
|
|
853
|
+
const entries = fs.readdirSync(dir);
|
|
854
|
+
const now = Date.now();
|
|
855
|
+
const tenMinutesMs = 10 * 60 * 1000;
|
|
856
|
+
for (const entry of entries) {
|
|
857
|
+
if (entry.includes('.tmp.')) {
|
|
858
|
+
const fullPath = path.join(dir, entry);
|
|
859
|
+
bestEffort('clean-tmp-file', () => {
|
|
860
|
+
const stat = fs.statSync(fullPath);
|
|
861
|
+
if (stat.isFile() && (now - stat.mtimeMs > tenMinutesMs)) {
|
|
862
|
+
fs.unlinkSync(fullPath);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch (dirErr) {
|
|
868
|
+
bestEffort('log-clean-tmp-dir-error', () => logger.debug?.(`[dsh-cron] clean orphaned tmp dir error: ${dirErr?.message}`));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Append rotated overflow history entries into tasks-history-archive.json (#134, #379, GH #17).
|
|
837
874
|
*/
|
|
838
875
|
_archiveHistoryRuns(id, overflowRuns) {
|
|
839
876
|
if (!Array.isArray(overflowRuns) || overflowRuns.length === 0) return;
|
|
@@ -845,14 +882,43 @@ export class TaskStore {
|
|
|
845
882
|
archive = bestEffort('parse-history-archive', () => JSON.parse(fs.readFileSync(archivePath, 'utf-8'))) || {};
|
|
846
883
|
}
|
|
847
884
|
if (!Array.isArray(archive[id])) archive[id] = [];
|
|
848
|
-
|
|
885
|
+
|
|
886
|
+
// Strip full raw diff payload from archived overflow runs to prevent runaway JSON growth (#379, GH #17)
|
|
887
|
+
const sanitizedOverflow = overflowRuns.map((r) => {
|
|
888
|
+
if (!r || !r.gitDiff) return r;
|
|
889
|
+
const { diff, ...gitDiffMeta } = r.gitDiff;
|
|
890
|
+
return {
|
|
891
|
+
...r,
|
|
892
|
+
gitDiff: {
|
|
893
|
+
...gitDiffMeta,
|
|
894
|
+
diffExcerpt: diff ? diff.slice(0, 500) : '',
|
|
895
|
+
hasChanges: Boolean(gitDiffMeta.hasChanges),
|
|
896
|
+
archived: true,
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
archive[id].push(...sanitizedOverflow);
|
|
849
902
|
if (archive[id].length > 1000) {
|
|
850
903
|
archive[id] = archive[id].slice(-1000);
|
|
851
904
|
}
|
|
905
|
+
|
|
906
|
+
// Enforce total archive size budget (max 15MB) across tasks (#379, GH #17)
|
|
907
|
+
const MAX_ARCHIVE_BYTES = 15 * 1024 * 1024;
|
|
908
|
+
let serialized = JSON.stringify(archive);
|
|
909
|
+
if (serialized.length > MAX_ARCHIVE_BYTES) {
|
|
910
|
+
for (const taskId of Object.keys(archive)) {
|
|
911
|
+
if (Array.isArray(archive[taskId]) && archive[taskId].length > 100) {
|
|
912
|
+
archive[taskId] = archive[taskId].slice(-100);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
serialized = JSON.stringify(archive);
|
|
916
|
+
}
|
|
917
|
+
|
|
852
918
|
// Write atomically with tmp file using compact JSON (avoiding mega-lines and null, 2 serialization bloat)
|
|
853
919
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
854
920
|
const tmp = `${archivePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
855
|
-
fs.writeFileSync(tmp,
|
|
921
|
+
fs.writeFileSync(tmp, serialized, { encoding: 'utf-8', mode: 0o600 });
|
|
856
922
|
bestEffort('chmod-tmp', () => fs.chmodSync(tmp, 0o600));
|
|
857
923
|
fs.renameSync(tmp, archivePath);
|
|
858
924
|
bestEffort('chmod-archive', () => fs.chmodSync(archivePath, 0o600));
|
package/lib/task-create.js
CHANGED
|
@@ -85,7 +85,8 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
85
85
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(args.misfirePolicy) ? args.misfirePolicy : 'skip',
|
|
86
86
|
maxRetries: Number(args.maxRetries) > 0 ? Number(args.maxRetries) : 0,
|
|
87
87
|
retryBackoffMs: Number(args.retryBackoffMs) > 0 ? Number(args.retryBackoffMs) : 30000,
|
|
88
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
88
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
89
|
+
captureGitDiff: typeof args.captureGitDiff === 'boolean' ? args.captureGitDiff : (typeof args.captureDiff === 'boolean' ? args.captureDiff : undefined),
|
|
89
90
|
kanbanMode: args.kanbanMode || 'none',
|
|
90
91
|
channels: Array.isArray(args.channels) ? args.channels.filter((c) => CHANNEL_IDS.includes(c)) : undefined,
|
|
91
92
|
template: args.template ? String(args.template) : '',
|
package/lib/task-patch.js
CHANGED
|
@@ -156,6 +156,16 @@ export function buildTaskPatch(current, body) {
|
|
|
156
156
|
if (patch.tokenLimit !== undefined) {
|
|
157
157
|
patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
|
|
158
158
|
}
|
|
159
|
+
if (patch.permissionPreset !== undefined) {
|
|
160
|
+
patch.permissionPreset = ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(patch.permissionPreset)
|
|
161
|
+
? patch.permissionPreset
|
|
162
|
+
: 'default';
|
|
163
|
+
}
|
|
164
|
+
if (patch.captureGitDiff !== undefined) {
|
|
165
|
+
patch.captureGitDiff = Boolean(patch.captureGitDiff);
|
|
166
|
+
} else if (patch.captureDiff !== undefined) {
|
|
167
|
+
patch.captureGitDiff = Boolean(patch.captureDiff);
|
|
168
|
+
}
|
|
159
169
|
const effectiveType = patch.type !== undefined ? normalizeTaskType(patch.type) : current.type;
|
|
160
170
|
if (EXTERNAL_RUNTIMES.includes(effectiveType) && patch.permissionPreset !== undefined) {
|
|
161
171
|
patch.permissionPreset = 'default';
|
package/lib/task-transfer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|