@indigoai-us/hq-cloud 6.18.16 → 6.18.18

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.
Files changed (82) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +26 -1
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-company.test.js +184 -3
  5. package/dist/bin/sync-runner-company.test.js.map +1 -1
  6. package/dist/bin/sync-runner.d.ts +13 -0
  7. package/dist/bin/sync-runner.d.ts.map +1 -1
  8. package/dist/bin/sync-runner.js.map +1 -1
  9. package/dist/bin/sync-runner.test.js +57 -2
  10. package/dist/bin/sync-runner.test.js.map +1 -1
  11. package/dist/cli/rescue-core.d.ts.map +1 -1
  12. package/dist/cli/rescue-core.js +21 -6
  13. package/dist/cli/rescue-core.js.map +1 -1
  14. package/dist/cli/rescue-rsync-status.test.js +23 -0
  15. package/dist/cli/rescue-rsync-status.test.js.map +1 -1
  16. package/dist/cli/share-plan-limit-presign.test.d.ts +2 -0
  17. package/dist/cli/share-plan-limit-presign.test.d.ts.map +1 -0
  18. package/dist/cli/share-plan-limit-presign.test.js +271 -0
  19. package/dist/cli/share-plan-limit-presign.test.js.map +1 -0
  20. package/dist/cli/share.d.ts +22 -0
  21. package/dist/cli/share.d.ts.map +1 -1
  22. package/dist/cli/share.js +151 -25
  23. package/dist/cli/share.js.map +1 -1
  24. package/dist/cli/share.test.js +262 -18
  25. package/dist/cli/share.test.js.map +1 -1
  26. package/dist/cli/sync.d.ts +63 -1
  27. package/dist/cli/sync.d.ts.map +1 -1
  28. package/dist/cli/sync.js +42 -1
  29. package/dist/cli/sync.js.map +1 -1
  30. package/dist/cli/sync.test.js +44 -0
  31. package/dist/cli/sync.test.js.map +1 -1
  32. package/dist/index.d.ts +5 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/object-io.d.ts +40 -0
  37. package/dist/object-io.d.ts.map +1 -1
  38. package/dist/object-io.js +122 -5
  39. package/dist/object-io.js.map +1 -1
  40. package/dist/object-io.test.js +144 -1
  41. package/dist/object-io.test.js.map +1 -1
  42. package/dist/plan-limit-pause.d.ts +61 -0
  43. package/dist/plan-limit-pause.d.ts.map +1 -0
  44. package/dist/plan-limit-pause.js +252 -0
  45. package/dist/plan-limit-pause.js.map +1 -0
  46. package/dist/plan-limit-pause.test.d.ts +2 -0
  47. package/dist/plan-limit-pause.test.d.ts.map +1 -0
  48. package/dist/plan-limit-pause.test.js +150 -0
  49. package/dist/plan-limit-pause.test.js.map +1 -0
  50. package/dist/plan-limit.d.ts +87 -0
  51. package/dist/plan-limit.d.ts.map +1 -0
  52. package/dist/plan-limit.fixtures.d.ts +34 -0
  53. package/dist/plan-limit.fixtures.d.ts.map +1 -0
  54. package/dist/plan-limit.fixtures.js +96 -0
  55. package/dist/plan-limit.fixtures.js.map +1 -0
  56. package/dist/plan-limit.js +157 -0
  57. package/dist/plan-limit.js.map +1 -0
  58. package/dist/plan-limit.test.d.ts +2 -0
  59. package/dist/plan-limit.test.d.ts.map +1 -0
  60. package/dist/plan-limit.test.js +132 -0
  61. package/dist/plan-limit.test.js.map +1 -0
  62. package/dist/sync/index.d.ts +1 -1
  63. package/dist/sync/index.d.ts.map +1 -1
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/mutation-client.d.ts +14 -2
  66. package/dist/sync/mutation-client.d.ts.map +1 -1
  67. package/dist/sync/mutation-client.js +34 -2
  68. package/dist/sync/mutation-client.js.map +1 -1
  69. package/dist/sync/mutation-client.test.js +49 -0
  70. package/dist/sync/mutation-client.test.js.map +1 -1
  71. package/dist/sync-progress.d.ts.map +1 -1
  72. package/dist/sync-progress.js +29 -0
  73. package/dist/sync-progress.js.map +1 -1
  74. package/dist/sync-progress.test.js +86 -0
  75. package/dist/sync-progress.test.js.map +1 -1
  76. package/dist/vault-client.d.ts +49 -6
  77. package/dist/vault-client.d.ts.map +1 -1
  78. package/dist/vault-client.js +115 -43
  79. package/dist/vault-client.js.map +1 -1
  80. package/dist/vault-client.test.js +86 -10
  81. package/dist/vault-client.test.js.map +1 -1
  82. package/package.json +1 -1
package/dist/cli/share.js CHANGED
@@ -13,7 +13,7 @@ import { createSyncProgressRecorder } from "../sync-progress.js";
13
13
  import { localPathForVaultKey, vaultKeyForLocalPath } from "../local-path-codec.js";
14
14
  import { uploadFile, uploadSymlink, toPosixKey, headRemoteFile, readVerifiedRemoteSymlinkTarget, deleteRemoteFile, downloadFile, primeObjectTransport, primeUploads, publishedContentHash, setUploadContentHash, WindowsSymlinkPrivilegeError, WINDOWS_SYMLINK_PRIVILEGE_REMEDY, WindowsRenameBlockedError, WINDOWS_RENAME_BLOCKED_REMEDY, renameOrRaiseWindowsBlocked, } from "../s3.js";
15
15
  import * as crypto from "crypto";
16
- import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, S3SdkObjectIO, } from "../object-io.js";
16
+ import { bindObjectIOToContext, PresignObjectIO, RateLimitedError, resolveObjectIO, S3SdkObjectIO, } from "../object-io.js";
17
17
  import { primePrivateFolderUpload } from "../private-folder-transport.js";
18
18
  import { isUnderAnyRoot, readJournalForPass, readJournalScoped, writeJournal, hashFile, hashSymlinkTarget, journalRows, journalEntryMatchesStat, journalFingerprintMatchesStat, dropJournalDecodeWindow, timestampsTooCoarseToTrust, updateEntry, PrematureJournalEntryError, removeEntry, isTombstone, normalizeEtag, PERSONAL_VAULT_JOURNAL_SLUG, migratePersonalVaultJournal, migrateToV2, } from "../journal.js";
19
19
  import { createIgnoreFilter, hasControlCharacters, isExpectedIgnore, isSecretPatternIgnore, isWithinSizeLimit, } from "../ignore.js";
@@ -32,7 +32,9 @@ import { isOntologyEntityKey, ONTOLOGY_MULTI_WRITER_FLAG, resolveOntologyEntityP
32
32
  import { entityIsFreshForCreate, outboxAllowsEntityPush, readOntologyOutbox, } from "../lib/ontology-outbox.js";
33
33
  import { resolveSkillRegisterLandedBytes } from "../lib/skill-register-landed-bytes.js";
34
34
  import { resolvePersonalConnectorSettingsSync } from "../lib/personal-connector-settings-sync.js";
35
- import { VaultAuthError, VaultClient, VaultClientError, VaultConflictError, VaultPermissionDeniedError, } from "../vault-client.js";
35
+ import { planLimitRefusalFromError, VaultAuthError, VaultClient, VaultClientError, VaultConflictError, VaultPermissionDeniedError, } from "../vault-client.js";
36
+ import { formatPlanLimitNotice } from "../plan-limit.js";
37
+ import { PlanLimitPauseStore, planLimitPauseNow, PLAN_LIMIT_PAUSE_RETRY_MS, } from "../plan-limit-pause.js";
36
38
  import { describeError } from "../lib/describe-error.js";
37
39
  import { isTransientHttpStatusError } from "../lib/net-errors.js";
38
40
  import { isRetryableIdentityRefusal, isTerminalIdentityRefusal, } from "../cognito-auth.js";
@@ -1243,6 +1245,8 @@ function createShareCounters() {
1243
1245
  filesRefusedStale: 0,
1244
1246
  filesSuppressedByTombstone: 0,
1245
1247
  uploadedPaths: [],
1248
+ filesPlanLimited: 0,
1249
+ planLimitedPaths: [],
1246
1250
  };
1247
1251
  }
1248
1252
  const CANONICAL_COMPANY_SKILL_RE = /^skills\/([a-z0-9][a-z0-9-]*)\/SKILL\.md$/;
@@ -2051,15 +2055,48 @@ async function refreshPushJournalAfterWalk(run, collectedKeys = [], deleteScopeR
2051
2055
  await yieldToCooperativeCheckpoint(checkpointWork, run.options.cooperativeCheckpoint, true);
2052
2056
  }
2053
2057
  }
2054
- function planLimitUpgradeUrlForError(error) {
2055
- if (!(error instanceof VaultClientError) || !error.upgradeUrl)
2056
- return undefined;
2057
- if (error.statusCode === 402 && error.code === "plan_limit_reached") {
2058
- return error.upgradeUrl;
2058
+ /** Refused paths carried on a `plan-limit` event and in `ShareResult`. */
2059
+ const PLAN_LIMITED_PATHS_CAP = 50;
2060
+ /**
2061
+ * One files admission check for a vault with paused uploads (see
2062
+ * `ObjectIO.checkFilesAdmission`). `allowed` clears the vault's pauses so the
2063
+ * files upload in this pass; `refused` keeps them and refreshes the stored
2064
+ * refusal; anything else keeps the hourly retry. Never throws: an unknown
2065
+ * answer is logged at debug level only.
2066
+ */
2067
+ async function checkPlanLimitAdmission(run, pauses) {
2068
+ if (!run.ctx.uid.startsWith("cmp_"))
2069
+ return;
2070
+ let outcome;
2071
+ try {
2072
+ const io = resolveObjectIO(run.ctx);
2073
+ if (typeof io.checkFilesAdmission !== "function")
2074
+ return;
2075
+ outcome = await io.checkFilesAdmission();
2076
+ }
2077
+ catch (err) {
2078
+ outcome = { state: "unknown", reason: describeError(err) };
2059
2079
  }
2060
- if (error.code === "PLAN_LIMIT_REACHED")
2061
- return error.upgradeUrl;
2062
- return undefined;
2080
+ if (outcome.state === "allowed") {
2081
+ pauses.clear();
2082
+ }
2083
+ else if (outcome.state === "refused") {
2084
+ pauses.refreshRefusal(outcome.refusal);
2085
+ }
2086
+ else {
2087
+ debugPlanLimit({
2088
+ event: "plan-limit.admission.unknown",
2089
+ companyUid: run.ctx.uid,
2090
+ reason: outcome.reason,
2091
+ });
2092
+ }
2093
+ }
2094
+ /** Debug-level diagnostic on stderr, only when `LOG_LEVEL` is debug/trace. */
2095
+ function debugPlanLimit(fields) {
2096
+ const level = process.env.LOG_LEVEL;
2097
+ if (level !== "debug" && level !== "trace")
2098
+ return;
2099
+ process.stderr.write(`${JSON.stringify({ level: "debug", component: "plan-limit", ...fields })}\n`);
2063
2100
  }
2064
2101
  async function executeUploads(run, pushPlan, counters, conflictPaths) {
2065
2102
  counters.filesSkipped += pushPlan.preplannedFilesSkipped;
@@ -2085,21 +2122,51 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2085
2122
  const entry = journalRows(run.journal).get(item.relativePath);
2086
2123
  return entry !== undefined && entry.hash === item.localHash;
2087
2124
  };
2125
+ // Plan-limit pauses (plan-limit-pause.ts). Read once per pass; the pass's
2126
+ // refusals and successes are applied in finishPlanLimitPass below.
2127
+ const planLimitPauses = PlanLimitPauseStore.open(run.ctx.uid);
2128
+ const planLimitPassStartedAt = planLimitPauseNow();
2129
+ const honorPlanLimitPauses = run.options.planLimitPause === true;
2130
+ const uploadedBeforePass = counters.filesUploaded;
2131
+ // Refused by the server in this pass.
2132
+ const planLimitRefusedPaths = new Set();
2133
+ // Skipped without a presign because an earlier refusal paused them, with
2134
+ // the end of each pause window.
2135
+ const planLimitPausedSkips = new Map();
2136
+ let planLimitRefusal;
2137
+ const notePlanLimitedPath = (relativePath) => {
2138
+ counters.filesPlanLimited++;
2139
+ if (counters.planLimitedPaths.length < PLAN_LIMITED_PATHS_CAP) {
2140
+ counters.planLimitedPaths.push(relativePath);
2141
+ }
2142
+ };
2143
+ const planLimitPausedUntil = (item) => honorPlanLimitPauses
2144
+ ? planLimitPauses.pausedUntil(item.relativePath, planLimitPassStartedAt)
2145
+ : undefined;
2146
+ const isPlanLimitPaused = (item) => planLimitPausedUntil(item) !== undefined;
2088
2147
  // Prime from the preview's likely uploads for transport efficiency. The
2089
2148
  // candidate is refreshed after the plan event and tombstone suppression is
2090
2149
  // applied BEFORE any presigns are minted. The queue below still re-plans
2091
2150
  // every preview item before admission, so a later edit cannot disappear from
2092
2151
  // the pass or carry a stale hash into its journal entry.
2093
- const initiallyPlannedUploads = [];
2152
+ const plannedUploadCandidates = [];
2094
2153
  for (const previewItem of pushPlan.items) {
2095
2154
  if (previewItem.action !== "upload")
2096
2155
  continue;
2097
2156
  const item = replanPushItemAtExecution(previewItem, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush);
2098
- if (item?.action === "upload" &&
2099
- !isUploadSuppressedByTombstone(item)) {
2100
- initiallyPlannedUploads.push(item);
2157
+ if (item?.action === "upload" && !isUploadSuppressedByTombstone(item)) {
2158
+ plannedUploadCandidates.push(item);
2101
2159
  }
2102
2160
  }
2161
+ // Before skipping paused files, ask the company vault once whether it
2162
+ // accepts uploads again (an upgrade, or files deleted to get under the
2163
+ // limit). Allowed: drop the pauses and upload them in this pass. Refused:
2164
+ // keep them. Anything else: keep the hourly retry.
2165
+ if (plannedUploadCandidates.some(isPlanLimitPaused)) {
2166
+ await checkPlanLimitAdmission(run, planLimitPauses);
2167
+ }
2168
+ // A paused file is not uploaded this pass, so do not presign it either.
2169
+ const initiallyPlannedUploads = plannedUploadCandidates.filter((item) => !isPlanLimitPaused(item));
2103
2170
  if (run.options.cooperativeCheckpoint?.shouldAbort?.() === true) {
2104
2171
  return {
2105
2172
  aborted: true,
@@ -2122,18 +2189,59 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2122
2189
  let aborted = false;
2123
2190
  let abortFlightConflictPaths = [];
2124
2191
  let hadUploadFailure = false;
2125
- let emittedPlanLimitNotice = false;
2126
- const emitPlanLimitSkip = (error) => {
2127
- const upgradeUrl = planLimitUpgradeUrlForError(error);
2128
- if (!upgradeUrl)
2192
+ // A plan-limit refusal is a deliberate per-file skip, never an error: the
2193
+ // file is counted, remembered for the end-of-pass notice, and paused.
2194
+ const emitPlanLimitSkip = (error, relativePath) => {
2195
+ const refusal = planLimitRefusalFromError(error);
2196
+ if (!refusal)
2129
2197
  return false;
2130
2198
  counters.filesSkipped++;
2131
- if (!emittedPlanLimitNotice) {
2132
- emittedPlanLimitNotice = true;
2133
- run.emit({ type: "plan-limit", upgradeUrl });
2199
+ planLimitRefusal ??= refusal;
2200
+ if (!planLimitRefusedPaths.has(relativePath)) {
2201
+ planLimitRefusedPaths.add(relativePath);
2202
+ notePlanLimitedPath(relativePath);
2134
2203
  }
2135
2204
  return true;
2136
2205
  };
2206
+ // Apply the pass's outcome to the pause record and report every file the
2207
+ // plan limit kept back in this pass, in ONE `plan-limit` event: files the
2208
+ // server refused now, and files skipped because an earlier refusal paused
2209
+ // them (`fromPause: true` when only the latter). A successful upload means
2210
+ // the server is accepting writes for this vault again, so earlier pauses
2211
+ // are dropped before this pass's refusals are recorded.
2212
+ const finishPlanLimitPass = () => {
2213
+ const now = planLimitPauseNow();
2214
+ // Read before clear(): the stored refusal describes the paused files.
2215
+ const storedRefusal = planLimitPauses.refusal;
2216
+ const pausesCleared = counters.filesUploaded > uploadedBeforePass;
2217
+ if (pausesCleared)
2218
+ planLimitPauses.clear();
2219
+ const refused = [...planLimitRefusedPaths];
2220
+ if (planLimitRefusal && refused.length > 0) {
2221
+ planLimitPauses.pause(refused, planLimitRefusal, now);
2222
+ }
2223
+ const pausedSkips = [...planLimitPausedSkips.keys()].filter((p) => !planLimitRefusedPaths.has(p));
2224
+ const refusal = planLimitRefusal ?? storedRefusal;
2225
+ if (refusal && refused.length + pausedSkips.length > 0) {
2226
+ counters.planLimit ??= refusal;
2227
+ // The earliest moment any of these files is retried. Paused files whose
2228
+ // pause was just cleared by a successful upload retry on the next pass.
2229
+ let retryAt = refused.length > 0 ? now + PLAN_LIMIT_PAUSE_RETRY_MS : Infinity;
2230
+ for (const p of pausedSkips) {
2231
+ retryAt = Math.min(retryAt, pausesCleared ? now : planLimitPausedSkips.get(p));
2232
+ }
2233
+ run.emit({
2234
+ type: "plan-limit",
2235
+ ...refusal,
2236
+ upgradeUrl: refusal.upgradeUrl,
2237
+ count: refused.length + pausedSkips.length,
2238
+ paths: [...refused, ...pausedSkips].slice(0, PLAN_LIMITED_PATHS_CAP),
2239
+ pausedUntil: new Date(retryAt).toISOString(),
2240
+ fromPause: refused.length === 0,
2241
+ });
2242
+ }
2243
+ planLimitPauses.save(now);
2244
+ };
2137
2245
  const settleFreshSkip = (item) => {
2138
2246
  if (item.action === "refuse-server-owned") {
2139
2247
  if (run.options.serverOwnedPathPolicy !== "skip") {
@@ -2782,7 +2890,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2782
2890
  // full-journal write after every later PUT.
2783
2891
  if (err instanceof JournalCheckpointError)
2784
2892
  throw err;
2785
- if (emitPlanLimitSkip(err))
2893
+ if (emitPlanLimitSkip(err, relativePath))
2786
2894
  return;
2787
2895
  if (isPreconditionFailed(err)) {
2788
2896
  conflictPaths.push(relativePath);
@@ -2837,7 +2945,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2837
2945
  }
2838
2946
  if (retryErr instanceof JournalCheckpointError)
2839
2947
  throw retryErr;
2840
- if (emitPlanLimitSkip(retryErr))
2948
+ if (emitPlanLimitSkip(retryErr, relativePath))
2841
2949
  return;
2842
2950
  if (isPreconditionFailed(retryErr)) {
2843
2951
  counters.filesSkipped++;
@@ -2925,7 +3033,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2925
3033
  isRetryableIdentityRefusal(retryErr)) {
2926
3034
  throw retryErr;
2927
3035
  }
2928
- if (emitPlanLimitSkip(retryErr))
3036
+ if (emitPlanLimitSkip(retryErr, relativePath))
2929
3037
  return;
2930
3038
  hadUploadFailure = true;
2931
3039
  run.emit({
@@ -3056,6 +3164,20 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3056
3164
  skippedSinceCooperativeCheckpoint++;
3057
3165
  continue;
3058
3166
  }
3167
+ const pausedUntil = planLimitPausedUntil(item);
3168
+ if (pausedUntil !== undefined) {
3169
+ // Refused by the plan limit within the last hour: skip it without a
3170
+ // presign until the pause lapses, the admission check clears it, or
3171
+ // an upload to this vault succeeds. Reported at the end of the pass.
3172
+ queue.shift();
3173
+ counters.filesSkipped++;
3174
+ if (!planLimitPausedSkips.has(item.relativePath)) {
3175
+ planLimitPausedSkips.set(item.relativePath, pausedUntil);
3176
+ notePlanLimitedPath(item.relativePath);
3177
+ }
3178
+ skippedSinceCooperativeCheckpoint++;
3179
+ continue;
3180
+ }
3059
3181
  // Reserve the file's CURRENT on-disk size, not the planned size: an
3060
3182
  // active log/session file can grow between planning and upload, and
3061
3183
  // uploadFile buffers the whole current body. A stale planned size would
@@ -3135,6 +3257,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3135
3257
  }
3136
3258
  }
3137
3259
  }
3260
+ finishPlanLimitPass();
3138
3261
  return { aborted, abortFlightConflictPaths, workerErrors, hadUploadFailure };
3139
3262
  }
3140
3263
  /**
@@ -3946,6 +4069,9 @@ function buildShareResult(run, counters, filesRefusedStalePaths, conflictPaths,
3946
4069
  linkedSubtreesNotShipped: [...run.linkedSubtreeSet],
3947
4070
  conflictPaths,
3948
4071
  pathResults,
4072
+ filesPlanLimited: counters.filesPlanLimited,
4073
+ planLimitedPaths: counters.planLimitedPaths,
4074
+ ...(counters.planLimit ? { planLimit: counters.planLimit } : {}),
3949
4075
  aborted,
3950
4076
  };
3951
4077
  }
@@ -3960,7 +4086,7 @@ function defaultConsoleLogger(event) {
3960
4086
  }
3961
4087
  }
3962
4088
  else if (event.type === "plan-limit") {
3963
- console.warn(` ! New files are paused by your plan. Upgrade: ${event.upgradeUrl}`);
4089
+ console.warn(formatPlanLimitNotice(event));
3964
4090
  }
3965
4091
  else if (event.type === "progress") {
3966
4092
  if (event.deleted) {