zames_pro 2.39.0 → 2.41.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.
@@ -3,6 +3,7 @@ import { loadProjectContext } from './context.js';
3
3
  import { getGitContext, formatGitContext } from './gitTools.js';
4
4
  import { parseXmlToolCalls } from './xml-toolcall.js';
5
5
  import { translate } from './i18n.js';
6
+ import { substituteAttachmentMarkers } from './commands.js';
6
7
  import { normText } from './browser.js';
7
8
  export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, }) {
8
9
  // UI callbacks must NEVER break the agent loop. A rendering error (a huge
@@ -117,20 +118,32 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
117
118
  // Tell the model what the markers mean even when the system-prompt is not
118
119
  // (re)sent (resumed chat). The files are uploaded to the chat by the
119
120
  // browser; this note just explains the [image#N] / [file#N] markers.
120
- const markers = attachments.map((a, i) => {
121
- const ext = String(a.name || '');
122
- const isImg = /\.(png|jpe?g|gif|webp|bmp|svg|ico)$/i.test(ext);
123
- return (isImg ? '[image#' : '[file#') + (i + 1) + ']';
121
+ //
122
+ // The task text keeps its COMPACT markers in the terminal, but what goes
123
+ // to the model is expanded to the real file paths (substituteAttachment
124
+ // Markers) so it can actually read them — a bare [file#1] is useless to
125
+ // the model. The note below still explains the marker→file mapping.
126
+ //
127
+ // Numbering matches AttachmentStore: images and files are counted
128
+ // SEPARATELY (image#1 and file#1 coexist), so the note lists them the
129
+ // same way the substitution resolves them.
130
+ let imgN = 0;
131
+ let fileN = 0;
132
+ const markers = attachments.map((a) => {
133
+ const isImg = String(a.mime || '').startsWith('image/') ||
134
+ /\.(png|jpe?g|gif|webp|bmp|svg|ico)$/i.test(String(a.name || ''));
135
+ return isImg ? '[image#' + ++imgN + ']' : '[file#' + ++fileN + ']';
124
136
  });
125
137
  message =
126
- task +
138
+ substituteAttachmentMarkers(task, attachments) +
127
139
  String.fromCharCode(10) +
128
140
  String.fromCharCode(10) +
129
141
  '(The user attached ' +
130
142
  attachments.length +
131
143
  ' file(s) to this message: ' +
132
144
  markers.join(', ') +
133
- '. Look at the uploaded images in the chat; file copies are in <project>/tmp.)';
145
+ '. The [image#N]/[file#N] markers were replaced above by the real file paths. ' +
146
+ 'Look at the uploaded images in the chat; file copies are in <project>/tmp.)';
134
147
  }
135
148
  transcript?.log('task', { task });
136
149
  let malformedRetries = 0;
@@ -139,6 +139,12 @@ export class AttachmentStore {
139
139
  this.fileCount = 0;
140
140
  }
141
141
  add(item) {
142
+ // De-duplicate by real path: pasting the SAME file twice must reuse one
143
+ // attachment (one marker, one upload), not create [file#1] and [file#2].
144
+ // The operator saw exactly that when inserting the same file repeatedly.
145
+ const existing = this.items.find((a) => a.path === item.path);
146
+ if (existing)
147
+ return existing;
142
148
  const image = isImageName(item.name) || String(item.mime).startsWith('image/');
143
149
  const index = image ? ++this.imageCount : ++this.fileCount;
144
150
  const marker = image ? '[image#' + index + ']' : '[file#' + index + ']';
package/dist/commands.js CHANGED
@@ -508,6 +508,30 @@ export function parseGoalCommand(text) {
508
508
  return { sub: 'clear' };
509
509
  return { sub: 'set', goal: rest };
510
510
  }
511
+ // Replace [image#N] / [file#N] markers in a task with the REAL file path, so
512
+ // the model (reading the message in the DeepSeek chat) gets a usable path
513
+ // instead of a bare marker. The terminal keeps showing the compact markers;
514
+ // only the text sent to the browser is expanded. Numbering must match
515
+ // AttachmentStore.add(): images and files are counted SEPARATELY, in the order
516
+ // the attachments were added. A marker with no matching attachment is left
517
+ // as-is (never silently dropped). Pure, so it is unit-tested.
518
+ export function substituteAttachmentMarkers(text, attachments) {
519
+ let imageCount = 0;
520
+ let fileCount = 0;
521
+ const imagePath = new Map();
522
+ const filePath = new Map();
523
+ for (const a of attachments || []) {
524
+ const isImg = String(a.mime || '').startsWith('image/') || isImageName(a.name);
525
+ if (isImg)
526
+ imagePath.set(++imageCount, a.path);
527
+ else
528
+ filePath.set(++fileCount, a.path);
529
+ }
530
+ let out = String(text ?? '');
531
+ out = out.replace(/\[image#(\d+)\]/g, (whole, n) => imagePath.get(Number(n)) ?? whole);
532
+ out = out.replace(/\[file#(\d+)\]/g, (whole, n) => filePath.get(Number(n)) ?? whole);
533
+ return out;
534
+ }
511
535
  // True when a `/config ...` line can be handled LIVE (while the agent is busy):
512
536
  // the text subcommands only touch config values, never the chat. The bare
513
537
  // `/config` and `/config menu` (which pause the editor and read keys) are NOT
package/dist/i18n.js CHANGED
@@ -237,6 +237,10 @@ const CATALOG = {
237
237
  ru: 'Использование: /cron "<мин> <час> <день> <мес> <день недели>" <задача>',
238
238
  en: 'Usage: /cron "<min> <hour> <dom> <month> <dow>" <task>',
239
239
  },
240
+ 'sched.loop_too_short': {
241
+ ru: 'Слишком часто: минимум {min} (троттлинг отправок 15с).',
242
+ en: 'Too frequent: minimum {min} (the send throttle is 15s).',
243
+ },
240
244
  'sched.added': {
241
245
  ru: '⏰ Задача #{id} добавлена ({when}): {task}',
242
246
  en: '⏰ Job #{id} added ({when}): {task}',
package/dist/index.js CHANGED
@@ -24,6 +24,10 @@ import { closeWeb } from './web.js';
24
24
  import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  const NL = String.fromCharCode(10);
27
+ // Floor for a /loop interval. A loop faster than the send throttle (15s)
28
+ // cannot do anything useful and would just hammer the rate limit, so we refuse
29
+ // it. It does NOT touch the throttle itself.
30
+ const MIN_LOOP_MS = 15_000;
27
31
  // True when a DeepSeek session/credentials were stored earlier (auth.json).
28
32
  // Used by /doctor to report that auto re-login is ready.
29
33
  function authMarkerExists() {
@@ -2387,6 +2391,15 @@ async function main() {
2387
2391
  console.error(theme.error(t('sched.loop_usage')));
2388
2392
  continue;
2389
2393
  }
2394
+ // A very short loop (e.g. "/loop 1s ...") would keep the agent sending
2395
+ // back-to-back and hit the rate limit. Enforce a floor; the send throttle
2396
+ // is 15s, so anything faster is pointless anyway.
2397
+ if (ms < MIN_LOOP_MS) {
2398
+ console.error(theme.error(t('sched.loop_too_short', {
2399
+ min: formatInterval(MIN_LOOP_MS),
2400
+ })));
2401
+ continue;
2402
+ }
2390
2403
  const job = scheduler.addLoop(ms, taskStr, Date.now());
2391
2404
  startScheduleTicker();
2392
2405
  console.log(theme.system(t('sched.added', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.39.0",
3
+ "version": "2.41.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {